From 53e0966f517c7d0601cdb055c97a0596cb177b3f Mon Sep 17 00:00:00 2001 From: Alexander Konietzko Date: Sat, 1 Jul 2023 16:46:08 +0200 Subject: [PATCH] Add event sourcing --- CleanArchitecture.Api/Program.cs | 6 +- .../Properties/launchSettings.json | 2 +- .../UpdateUserCommandHandlerTests.cs | 34 ++++++++ .../UpdateUserCommandTestFixture.cs | 2 +- .../CommandHandlerFixtureBase.cs | 1 + CleanArchitecture.Domain/ApiUser.cs | 38 ++++++++- .../UpdateUser/UpdateUserCommandHandler.cs | 15 ++++ CleanArchitecture.Domain/DomainEvent.cs | 14 --- .../DomainEvents/DomainEvent.cs | 19 +++++ .../DomainEvents/IDomainEventStore.cs | 8 ++ .../DomainEvents/Message.cs | 22 +++++ .../DomainEvents/StoredDomainEvent.cs | 28 ++++++ .../StoredDomainNotification.cs | 35 ++++++++ .../Events/User/PasswordChangedEvent.cs | 1 + .../Events/User/UserCreatedEvent.cs | 1 + .../Events/User/UserDeletedEvent.cs | 1 + .../Events/User/UserUpdatedEvent.cs | 1 + .../Interfaces/IMediatorHandler.cs | 1 + CleanArchitecture.Domain/Interfaces/IUser.cs | 1 + .../Notifications/DomainNotification.cs | 4 +- .../InMemoryBusTests.cs | 10 ++- .../CleanArchitecture.Infrastructure.csproj | 1 + .../StoredDomainEventConfiguration.cs | 25 ++++++ .../StoredDomainNotificationConfiguration.cs | 42 +++++++++ .../DomainNotificationStoreDbContext.cs | 21 +++++ .../Database/EventStoreDbContext.cs | 21 +++++ .../EventSourcing/EventStore.cs | 56 ++++++++++++ .../EventSourcing/EventStoreContext.cs | 30 +++++++ .../EventSourcing/IEventStoreContext.cs | 7 ++ .../Extensions/ServiceCollectionExtensions.cs | 31 ++++++- .../InMemoryBus.cs | 10 ++- ...523_AddDomainNotificationStore.Designer.cs | 85 +++++++++++++++++++ ...230701135523_AddDomainNotificationStore.cs | 43 ++++++++++ ...NotificationStoreDbContextModelSnapshot.cs | 82 ++++++++++++++++++ .../20230701135441_AddEventStore.Designer.cs | 67 +++++++++++++++ .../20230701135441_AddEventStore.cs | 39 +++++++++ .../EventStoreDbContextModelSnapshot.cs | 64 ++++++++++++++ 37 files changed, 840 insertions(+), 28 deletions(-) delete mode 100644 CleanArchitecture.Domain/DomainEvent.cs create mode 100644 CleanArchitecture.Domain/DomainEvents/DomainEvent.cs create mode 100644 CleanArchitecture.Domain/DomainEvents/IDomainEventStore.cs create mode 100644 CleanArchitecture.Domain/DomainEvents/Message.cs create mode 100644 CleanArchitecture.Domain/DomainEvents/StoredDomainEvent.cs create mode 100644 CleanArchitecture.Domain/DomainNotifications/StoredDomainNotification.cs create mode 100644 CleanArchitecture.Infrastructure/Configurations/EventSourcing/StoredDomainEventConfiguration.cs create mode 100644 CleanArchitecture.Infrastructure/Configurations/EventSourcing/StoredDomainNotificationConfiguration.cs create mode 100644 CleanArchitecture.Infrastructure/Database/DomainNotificationStoreDbContext.cs create mode 100644 CleanArchitecture.Infrastructure/Database/EventStoreDbContext.cs create mode 100644 CleanArchitecture.Infrastructure/EventSourcing/EventStore.cs create mode 100644 CleanArchitecture.Infrastructure/EventSourcing/EventStoreContext.cs create mode 100644 CleanArchitecture.Infrastructure/EventSourcing/IEventStoreContext.cs create mode 100644 CleanArchitecture.Infrastructure/Migrations/DomainNotificationStoreDb/20230701135523_AddDomainNotificationStore.Designer.cs create mode 100644 CleanArchitecture.Infrastructure/Migrations/DomainNotificationStoreDb/20230701135523_AddDomainNotificationStore.cs create mode 100644 CleanArchitecture.Infrastructure/Migrations/DomainNotificationStoreDb/DomainNotificationStoreDbContextModelSnapshot.cs create mode 100644 CleanArchitecture.Infrastructure/Migrations/EventStoreDb/20230701135441_AddEventStore.Designer.cs create mode 100644 CleanArchitecture.Infrastructure/Migrations/EventStoreDb/20230701135441_AddEventStore.cs create mode 100644 CleanArchitecture.Infrastructure/Migrations/EventStoreDb/EventStoreDbContextModelSnapshot.cs diff --git a/CleanArchitecture.Api/Program.cs b/CleanArchitecture.Api/Program.cs index a4bc4a9..b176e0a 100644 --- a/CleanArchitecture.Api/Program.cs +++ b/CleanArchitecture.Api/Program.cs @@ -40,7 +40,7 @@ builder.Services.AddDbContext(options => builder.Services.AddSwagger(); builder.Services.AddAuth(builder.Configuration); -builder.Services.AddInfrastructure(); +builder.Services.AddInfrastructure(builder.Configuration, "CleanArchitecture.Infrastructure"); builder.Services.AddQueryHandlers(); builder.Services.AddServices(); builder.Services.AddCommandHandlers(); @@ -73,8 +73,12 @@ using (var scope = app.Services.CreateScope()) { var services = scope.ServiceProvider; var appDbContext = services.GetRequiredService(); + var storeDbContext = services.GetRequiredService(); + var domainStoreDbContext = services.GetRequiredService(); appDbContext.EnsureMigrationsApplied(); + storeDbContext.EnsureMigrationsApplied(); + domainStoreDbContext.EnsureMigrationsApplied(); } app.Run(); diff --git a/CleanArchitecture.Api/Properties/launchSettings.json b/CleanArchitecture.Api/Properties/launchSettings.json index 9894d69..7a96097 100644 --- a/CleanArchitecture.Api/Properties/launchSettings.json +++ b/CleanArchitecture.Api/Properties/launchSettings.json @@ -14,7 +14,7 @@ "dotnetRunMessages": true, "launchBrowser": false, "launchUrl": "swagger", - "applicationUrl": "https://localhost:7260;http://localhost:5201", + "applicationUrl": "https://localhost:7001;http://localhost:7000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs index 98a13df..2e1885b 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandHandlerTests.cs @@ -4,6 +4,7 @@ using CleanArchitecture.Domain.Commands.Users.UpdateUser; using CleanArchitecture.Domain.Enums; using CleanArchitecture.Domain.Errors; using CleanArchitecture.Domain.Events.User; +using Moq; using Xunit; namespace CleanArchitecture.Domain.Tests.CommandHandler.User.UpdateUser; @@ -54,4 +55,37 @@ public sealed class UpdateUserCommandHandlerTests ErrorCodes.ObjectNotFound, $"There is no User with Id {command.UserId}"); } + + [Fact] + public async Task Should_Not_Update_With_Existing_User_Email() + { + var user = _fixture.SetupUser(); + + var command = new UpdateUserCommand( + user.Id, + "test@email.com", + "Test", + "Email", + UserRole.User); + + _fixture.UserRepository + .Setup(x => x.GetByEmailAsync(command.Email)) + .ReturnsAsync(new Entities.User( + Guid.NewGuid(), + command.Email, + "Some", + "User", + "234fs@#*@#", + UserRole.User)); + + await _fixture.CommandHandler.Handle(command, default); + + _fixture + .VerifyNoCommit() + .VerifyNoRaisedEvent() + .VerifyAnyDomainNotification() + .VerifyExistingNotification( + DomainErrorCodes.UserAlreadyExists, + $"There is already a User with Email {command.Email}"); + } } \ No newline at end of file diff --git a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandTestFixture.cs b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandTestFixture.cs index 69bb3ca..6d94edb 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandTestFixture.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandler/User/UpdateUser/UpdateUserCommandTestFixture.cs @@ -21,7 +21,7 @@ public sealed class UpdateUserCommandTestFixture : CommandHandlerFixtureBase } public UpdateUserCommandHandler CommandHandler { get; } - private Mock UserRepository { get; } + public Mock UserRepository { get; } public Entities.User SetupUser() { diff --git a/CleanArchitecture.Domain.Tests/CommandHandlerFixtureBase.cs b/CleanArchitecture.Domain.Tests/CommandHandlerFixtureBase.cs index ce8a493..841fa1c 100644 --- a/CleanArchitecture.Domain.Tests/CommandHandlerFixtureBase.cs +++ b/CleanArchitecture.Domain.Tests/CommandHandlerFixtureBase.cs @@ -1,5 +1,6 @@ using System; using System.Linq.Expressions; +using CleanArchitecture.Domain.DomainEvents; using CleanArchitecture.Domain.Enums; using CleanArchitecture.Domain.Interfaces; using CleanArchitecture.Domain.Notifications; diff --git a/CleanArchitecture.Domain/ApiUser.cs b/CleanArchitecture.Domain/ApiUser.cs index 285bfd6..b6ac828 100644 --- a/CleanArchitecture.Domain/ApiUser.cs +++ b/CleanArchitecture.Domain/ApiUser.cs @@ -11,6 +11,9 @@ public sealed class ApiUser : IUser { private readonly IHttpContextAccessor _httpContextAccessor; + private string? _name = null; + private Guid _userId = Guid.Empty; + public ApiUser(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; @@ -18,11 +21,17 @@ public sealed class ApiUser : IUser public Guid GetUserId() { + if (_userId != Guid.Empty) + { + return _userId; + } + var claim = _httpContextAccessor.HttpContext?.User.Claims .FirstOrDefault(x => string.Equals(x.Type, ClaimTypes.NameIdentifier)); if (Guid.TryParse(claim?.Value, out var userId)) { + _userId = userId; return userId; } @@ -42,7 +51,32 @@ public sealed class ApiUser : IUser throw new ArgumentException("Could not parse user role"); } - public string Name => _httpContextAccessor.HttpContext?.User.Identity?.Name ?? string.Empty; + public string Name + { + get + { + if (_name != null) + { + return _name; + } + var identity = _httpContextAccessor.HttpContext?.User.Identity; + if (identity == null) + { + _name = string.Empty; + return string.Empty; + } + if (!string.IsNullOrWhiteSpace(identity.Name)) + { + _name = identity.Name; + return identity.Name; + } + var claim = _httpContextAccessor.HttpContext!.User.Claims + .FirstOrDefault(c => string.Equals(c.Type, "name", StringComparison.OrdinalIgnoreCase))? + .Value; + _name = claim ?? string.Empty; + return _name; + } + } public string GetUserEmail() { @@ -54,6 +88,6 @@ public sealed class ApiUser : IUser return claim.Value; } - throw new ArgumentException("Could not parse user email"); + return string.Empty; } } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs index cc40b23..cf0227e 100644 --- a/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs +++ b/CleanArchitecture.Domain/Commands/Users/UpdateUser/UpdateUserCommandHandler.cs @@ -57,6 +57,21 @@ public sealed class UpdateUserCommandHandler : CommandHandlerBase, return; } + if (request.Email != user.Email) + { + var existingUser = await _userRepository.GetByEmailAsync(request.Email); + + if (existingUser != null) + { + await Bus.RaiseEventAsync( + new DomainNotification( + request.MessageType, + $"There is already a User with Email {request.Email}", + DomainErrorCodes.UserAlreadyExists)); + return; + } + } + if (_user.GetUserRole() == UserRole.Admin) { user.SetRole(request.Role); diff --git a/CleanArchitecture.Domain/DomainEvent.cs b/CleanArchitecture.Domain/DomainEvent.cs deleted file mode 100644 index 678edd7..0000000 --- a/CleanArchitecture.Domain/DomainEvent.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using MediatR; - -namespace CleanArchitecture.Domain; - -public abstract class DomainEvent : INotification -{ - protected DomainEvent(Guid aggregateId) - { - Timestamp = DateTime.Now; - } - - private DateTime Timestamp { get; } -} \ No newline at end of file diff --git a/CleanArchitecture.Domain/DomainEvents/DomainEvent.cs b/CleanArchitecture.Domain/DomainEvents/DomainEvent.cs new file mode 100644 index 0000000..e5c25a9 --- /dev/null +++ b/CleanArchitecture.Domain/DomainEvents/DomainEvent.cs @@ -0,0 +1,19 @@ +using System; +using MediatR; + +namespace CleanArchitecture.Domain.DomainEvents; + +public abstract class DomainEvent : Message, INotification +{ + protected DomainEvent(Guid aggregateId) : base(aggregateId) + { + Timestamp = DateTime.Now; + } + + protected DomainEvent(Guid aggregateId, string? messageType) : base(aggregateId, messageType) + { + Timestamp = DateTime.Now; + } + + public DateTime Timestamp { get; private set; } +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/DomainEvents/IDomainEventStore.cs b/CleanArchitecture.Domain/DomainEvents/IDomainEventStore.cs new file mode 100644 index 0000000..68b17b5 --- /dev/null +++ b/CleanArchitecture.Domain/DomainEvents/IDomainEventStore.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace CleanArchitecture.Domain.DomainEvents; + +public interface IDomainEventStore +{ + Task SaveAsync(T domainEvent) where T : DomainEvent; +} \ No newline at end of file diff --git a/CleanArchitecture.Domain/DomainEvents/Message.cs b/CleanArchitecture.Domain/DomainEvents/Message.cs new file mode 100644 index 0000000..606c259 --- /dev/null +++ b/CleanArchitecture.Domain/DomainEvents/Message.cs @@ -0,0 +1,22 @@ +using System; +using MediatR; + +namespace CleanArchitecture.Domain.DomainEvents; + +public abstract class Message : IRequest +{ + protected Message(Guid aggregateId) + { + AggregateId = aggregateId; + MessageType = GetType().Name; + } + + protected Message(Guid aggregateId, string? messageType) + { + AggregateId = aggregateId; + MessageType = messageType ?? string.Empty; + } + + public Guid AggregateId { get; private set; } + public string MessageType { get; protected set; } +} diff --git a/CleanArchitecture.Domain/DomainEvents/StoredDomainEvent.cs b/CleanArchitecture.Domain/DomainEvents/StoredDomainEvent.cs new file mode 100644 index 0000000..1074410 --- /dev/null +++ b/CleanArchitecture.Domain/DomainEvents/StoredDomainEvent.cs @@ -0,0 +1,28 @@ +using System; + +namespace CleanArchitecture.Domain.DomainEvents; + +public class StoredDomainEvent : DomainEvent +{ + public Guid Id { get; private set; } + public string Data { get; private set; } = string.Empty; + public string User { get; private set; } = string.Empty; + public string CorrelationId { get; private set; } = string.Empty; + + public StoredDomainEvent( + DomainEvent domainEvent, + string data, + string user, + string correlationId) + : base(domainEvent.AggregateId, domainEvent.MessageType) + { + Id = Guid.NewGuid(); + Data = data; + User = user; + CorrelationId = correlationId; + } + + // EF Constructor + protected StoredDomainEvent() : base(Guid.NewGuid()) + { } +} diff --git a/CleanArchitecture.Domain/DomainNotifications/StoredDomainNotification.cs b/CleanArchitecture.Domain/DomainNotifications/StoredDomainNotification.cs new file mode 100644 index 0000000..3a6199c --- /dev/null +++ b/CleanArchitecture.Domain/DomainNotifications/StoredDomainNotification.cs @@ -0,0 +1,35 @@ +using System; +using CleanArchitecture.Domain.Notifications; + +namespace CleanArchitecture.Domain.DomainNotifications; + +public class StoredDomainNotification : DomainNotification +{ + public Guid Id { get; private set; } + public string SerializedData { get; private set; } = string.Empty; + public string User { get; private set; } = string.Empty; + public string CorrelationId { get; private set; } = string.Empty; + + public StoredDomainNotification( + DomainNotification domainNotification, + string data, + string user, + string correlationId) : base( + domainNotification.Key, + domainNotification.Value, + domainNotification.Code, + null, + domainNotification.AggregateId) + { + Id = Guid.NewGuid(); + User = user; + SerializedData = data; + CorrelationId = correlationId; + + MessageType = domainNotification.MessageType; + } + + // EF Constructor + protected StoredDomainNotification() : base(string.Empty, string.Empty, string.Empty) + { } +} diff --git a/CleanArchitecture.Domain/Events/User/PasswordChangedEvent.cs b/CleanArchitecture.Domain/Events/User/PasswordChangedEvent.cs index 7ebb021..e3bf3f1 100644 --- a/CleanArchitecture.Domain/Events/User/PasswordChangedEvent.cs +++ b/CleanArchitecture.Domain/Events/User/PasswordChangedEvent.cs @@ -1,4 +1,5 @@ using System; +using CleanArchitecture.Domain.DomainEvents; namespace CleanArchitecture.Domain.Events.User; diff --git a/CleanArchitecture.Domain/Events/User/UserCreatedEvent.cs b/CleanArchitecture.Domain/Events/User/UserCreatedEvent.cs index 2a67c9b..f21e681 100644 --- a/CleanArchitecture.Domain/Events/User/UserCreatedEvent.cs +++ b/CleanArchitecture.Domain/Events/User/UserCreatedEvent.cs @@ -1,4 +1,5 @@ using System; +using CleanArchitecture.Domain.DomainEvents; namespace CleanArchitecture.Domain.Events.User; diff --git a/CleanArchitecture.Domain/Events/User/UserDeletedEvent.cs b/CleanArchitecture.Domain/Events/User/UserDeletedEvent.cs index 89fadc1..5245879 100644 --- a/CleanArchitecture.Domain/Events/User/UserDeletedEvent.cs +++ b/CleanArchitecture.Domain/Events/User/UserDeletedEvent.cs @@ -1,4 +1,5 @@ using System; +using CleanArchitecture.Domain.DomainEvents; namespace CleanArchitecture.Domain.Events.User; diff --git a/CleanArchitecture.Domain/Events/User/UserUpdatedEvent.cs b/CleanArchitecture.Domain/Events/User/UserUpdatedEvent.cs index 92a09cf..d78cd72 100644 --- a/CleanArchitecture.Domain/Events/User/UserUpdatedEvent.cs +++ b/CleanArchitecture.Domain/Events/User/UserUpdatedEvent.cs @@ -1,4 +1,5 @@ using System; +using CleanArchitecture.Domain.DomainEvents; namespace CleanArchitecture.Domain.Events.User; diff --git a/CleanArchitecture.Domain/Interfaces/IMediatorHandler.cs b/CleanArchitecture.Domain/Interfaces/IMediatorHandler.cs index 22ca8ce..e8c5ef4 100644 --- a/CleanArchitecture.Domain/Interfaces/IMediatorHandler.cs +++ b/CleanArchitecture.Domain/Interfaces/IMediatorHandler.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using CleanArchitecture.Domain.Commands; +using CleanArchitecture.Domain.DomainEvents; using MediatR; namespace CleanArchitecture.Domain.Interfaces; diff --git a/CleanArchitecture.Domain/Interfaces/IUser.cs b/CleanArchitecture.Domain/Interfaces/IUser.cs index 26cbe1a..1a6f9e1 100644 --- a/CleanArchitecture.Domain/Interfaces/IUser.cs +++ b/CleanArchitecture.Domain/Interfaces/IUser.cs @@ -8,4 +8,5 @@ public interface IUser string Name { get; } Guid GetUserId(); UserRole GetUserRole(); + string GetUserEmail(); } \ No newline at end of file diff --git a/CleanArchitecture.Domain/Notifications/DomainNotification.cs b/CleanArchitecture.Domain/Notifications/DomainNotification.cs index e57a891..f3da784 100644 --- a/CleanArchitecture.Domain/Notifications/DomainNotification.cs +++ b/CleanArchitecture.Domain/Notifications/DomainNotification.cs @@ -1,8 +1,9 @@ using System; +using CleanArchitecture.Domain.DomainEvents; namespace CleanArchitecture.Domain.Notifications; -public sealed class DomainNotification : DomainEvent +public class DomainNotification : DomainEvent { public DomainNotification( string key, @@ -23,4 +24,5 @@ public sealed class DomainNotification : DomainEvent public string Value { get; } public string Code { get; } public object? Data { get; set; } + public int Version { get; private set; } = 1; } \ No newline at end of file diff --git a/CleanArchitecture.Infrastructure.Tests/InMemoryBusTests.cs b/CleanArchitecture.Infrastructure.Tests/InMemoryBusTests.cs index 382ccc2..d6916e3 100644 --- a/CleanArchitecture.Infrastructure.Tests/InMemoryBusTests.cs +++ b/CleanArchitecture.Infrastructure.Tests/InMemoryBusTests.cs @@ -2,6 +2,7 @@ using System; using System.Threading; using System.Threading.Tasks; using CleanArchitecture.Domain.Commands.Users.DeleteUser; +using CleanArchitecture.Domain.DomainEvents; using CleanArchitecture.Domain.Events.User; using CleanArchitecture.Domain.Notifications; using MediatR; @@ -16,8 +17,9 @@ public sealed class InMemoryBusTests public async Task InMemoryBus_Should_Publish_When_Not_DomainNotification() { var mediator = new Mock(); + var domainEventStore = new Mock(); - var inMemoryBus = new InMemoryBus(mediator.Object); + var inMemoryBus = new InMemoryBus(mediator.Object, domainEventStore.Object); const string key = "Key"; const string value = "Value"; @@ -34,8 +36,9 @@ public sealed class InMemoryBusTests public async Task InMemoryBus_Should_Save_And_Publish_When_DomainNotification() { var mediator = new Mock(); + var domainEventStore = new Mock(); - var inMemoryBus = new InMemoryBus(mediator.Object); + var inMemoryBus = new InMemoryBus(mediator.Object, domainEventStore.Object); var userDeletedEvent = new UserDeletedEvent(Guid.NewGuid()); @@ -48,8 +51,9 @@ public sealed class InMemoryBusTests public async Task InMemoryBus_Should_Send_Command_Async() { var mediator = new Mock(); + var domainEventStore = new Mock(); - var inMemoryBus = new InMemoryBus(mediator.Object); + var inMemoryBus = new InMemoryBus(mediator.Object, domainEventStore.Object); var deleteUserCommand = new DeleteUserCommand(Guid.NewGuid()); diff --git a/CleanArchitecture.Infrastructure/CleanArchitecture.Infrastructure.csproj b/CleanArchitecture.Infrastructure/CleanArchitecture.Infrastructure.csproj index e208a86..a95d6d9 100644 --- a/CleanArchitecture.Infrastructure/CleanArchitecture.Infrastructure.csproj +++ b/CleanArchitecture.Infrastructure/CleanArchitecture.Infrastructure.csproj @@ -18,6 +18,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/CleanArchitecture.Infrastructure/Configurations/EventSourcing/StoredDomainEventConfiguration.cs b/CleanArchitecture.Infrastructure/Configurations/EventSourcing/StoredDomainEventConfiguration.cs new file mode 100644 index 0000000..884137e --- /dev/null +++ b/CleanArchitecture.Infrastructure/Configurations/EventSourcing/StoredDomainEventConfiguration.cs @@ -0,0 +1,25 @@ +using CleanArchitecture.Domain.DomainEvents; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore; + +namespace CleanArchitecture.Infrastructure.Configurations.EventSourcing; + +public sealed class StoredDomainEventConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.Property(c => c.Timestamp) + .HasColumnName("CreationDate"); + + builder.Property(c => c.MessageType) + .HasColumnName("Action") + .HasColumnType("varchar(100)"); + + builder.Property(c => c.CorrelationId) + .HasMaxLength(100); + + builder.Property(c => c.User) + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + } +} diff --git a/CleanArchitecture.Infrastructure/Configurations/EventSourcing/StoredDomainNotificationConfiguration.cs b/CleanArchitecture.Infrastructure/Configurations/EventSourcing/StoredDomainNotificationConfiguration.cs new file mode 100644 index 0000000..47194e0 --- /dev/null +++ b/CleanArchitecture.Infrastructure/Configurations/EventSourcing/StoredDomainNotificationConfiguration.cs @@ -0,0 +1,42 @@ +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore; +using CleanArchitecture.Domain.DomainNotifications; + +namespace CleanArchitecture.Infrastructure.Configurations.EventSourcing; + +public sealed class StoredDomainNotificationConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.Property(c => c.MessageType) + .IsRequired() + .HasMaxLength(100); + + builder.Property(c => c.Key) + .IsRequired() + .HasMaxLength(100); + + builder.Property(c => c.Value) + .HasMaxLength(1024); + + builder.Property(c => c.Code) + .IsRequired() + .HasMaxLength(100); + + builder.Property(c => c.SerializedData) + .IsRequired(); + + builder.Property(c => c.User) + .IsRequired() + .HasMaxLength(100); + + builder.Property(c => c.CorrelationId) + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + builder.Ignore(c => c.Data); + + builder.Property(c => c.SerializedData) + .HasColumnName("Data"); + } +} diff --git a/CleanArchitecture.Infrastructure/Database/DomainNotificationStoreDbContext.cs b/CleanArchitecture.Infrastructure/Database/DomainNotificationStoreDbContext.cs new file mode 100644 index 0000000..7002617 --- /dev/null +++ b/CleanArchitecture.Infrastructure/Database/DomainNotificationStoreDbContext.cs @@ -0,0 +1,21 @@ +using CleanArchitecture.Domain.DomainNotifications; +using CleanArchitecture.Infrastructure.Configurations.EventSourcing; +using Microsoft.EntityFrameworkCore; + +namespace CleanArchitecture.Infrastructure.Database; + +public class DomainNotificationStoreDbContext : DbContext +{ + public DomainNotificationStoreDbContext(DbContextOptions options) : base(options) + { + } + + public virtual DbSet StoredDomainNotifications { get; set; } = null!; + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.ApplyConfiguration(new StoredDomainNotificationConfiguration()); + } +} \ No newline at end of file diff --git a/CleanArchitecture.Infrastructure/Database/EventStoreDbContext.cs b/CleanArchitecture.Infrastructure/Database/EventStoreDbContext.cs new file mode 100644 index 0000000..fb6a4b3 --- /dev/null +++ b/CleanArchitecture.Infrastructure/Database/EventStoreDbContext.cs @@ -0,0 +1,21 @@ +using CleanArchitecture.Domain.DomainEvents; +using CleanArchitecture.Infrastructure.Configurations.EventSourcing; +using Microsoft.EntityFrameworkCore; + +namespace CleanArchitecture.Infrastructure.Database; + +public class EventStoreDbContext : DbContext +{ + public EventStoreDbContext(DbContextOptions options) : base(options) + { + } + + public virtual DbSet StoredDomainEvents { get; set; } = null!; + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.ApplyConfiguration(new StoredDomainEventConfiguration()); + + base.OnModelCreating(modelBuilder); + } +} diff --git a/CleanArchitecture.Infrastructure/EventSourcing/EventStore.cs b/CleanArchitecture.Infrastructure/EventSourcing/EventStore.cs new file mode 100644 index 0000000..3343044 --- /dev/null +++ b/CleanArchitecture.Infrastructure/EventSourcing/EventStore.cs @@ -0,0 +1,56 @@ +using CleanArchitecture.Domain.DomainEvents; +using CleanArchitecture.Domain.DomainNotifications; +using System.Threading.Tasks; +using CleanArchitecture.Domain.Notifications; +using CleanArchitecture.Infrastructure.Database; +using Newtonsoft.Json; + +namespace CleanArchitecture.Infrastructure.EventSourcing; + +public class DomainEventStore : IDomainEventStore +{ + private readonly EventStoreDbContext _eventStoreDbContext; + private readonly DomainNotificationStoreDbContext _domainNotificationStoreDbContext; + private readonly IEventStoreContext _context; + + public DomainEventStore( + EventStoreDbContext eventStoreDbContext, + DomainNotificationStoreDbContext domainNotificationStoreDbContext, + IEventStoreContext context) + { + _eventStoreDbContext = eventStoreDbContext; + _domainNotificationStoreDbContext = domainNotificationStoreDbContext; + _context = context; + } + + public async Task SaveAsync(T domainEvent) where T : DomainEvent + { + var serializedData = JsonConvert.SerializeObject(domainEvent); + + switch (domainEvent) + { + case DomainNotification d: + var storedDomainNotification = new StoredDomainNotification( + d, + serializedData, + _context.GetUserEmail(), + _context.GetCorrelationId()); + + _domainNotificationStoreDbContext.StoredDomainNotifications.Add(storedDomainNotification); + await _domainNotificationStoreDbContext.SaveChangesAsync(); + + break; + default: + var storedDomainEvent = new StoredDomainEvent( + domainEvent, + serializedData, + _context.GetUserEmail(), + _context.GetCorrelationId()); + + _eventStoreDbContext.StoredDomainEvents.Add(storedDomainEvent); + await _eventStoreDbContext.SaveChangesAsync(); + + break; + } + } +} diff --git a/CleanArchitecture.Infrastructure/EventSourcing/EventStoreContext.cs b/CleanArchitecture.Infrastructure/EventSourcing/EventStoreContext.cs new file mode 100644 index 0000000..0edaffa --- /dev/null +++ b/CleanArchitecture.Infrastructure/EventSourcing/EventStoreContext.cs @@ -0,0 +1,30 @@ +using System; +using CleanArchitecture.Domain.Interfaces; +using Microsoft.AspNetCore.Http; + +namespace CleanArchitecture.Infrastructure.EventSourcing; + +public sealed class EventStoreContext : IEventStoreContext +{ + private readonly string _correlationId; + private readonly IUser? _user; + + public EventStoreContext(IUser? user, IHttpContextAccessor? httpContextAccessor) + { + _user = user; + + if (httpContextAccessor?.HttpContext == null || + !httpContextAccessor.HttpContext.Request.Headers.TryGetValue("X-CLEAN-ARCHITECTURE-CORRELATION-ID", out var id)) + { + _correlationId = $"internal - {Guid.NewGuid()}"; + } + else + { + _correlationId = id.ToString(); + } + } + + public string GetCorrelationId() => _correlationId; + + public string GetUserEmail() => _user?.GetUserEmail() ?? string.Empty; +} diff --git a/CleanArchitecture.Infrastructure/EventSourcing/IEventStoreContext.cs b/CleanArchitecture.Infrastructure/EventSourcing/IEventStoreContext.cs new file mode 100644 index 0000000..ab493f9 --- /dev/null +++ b/CleanArchitecture.Infrastructure/EventSourcing/IEventStoreContext.cs @@ -0,0 +1,7 @@ +namespace CleanArchitecture.Infrastructure.EventSourcing; + +public interface IEventStoreContext +{ + public string GetUserEmail(); + public string GetCorrelationId(); +} diff --git a/CleanArchitecture.Infrastructure/Extensions/ServiceCollectionExtensions.cs b/CleanArchitecture.Infrastructure/Extensions/ServiceCollectionExtensions.cs index a7a68a8..02d063f 100644 --- a/CleanArchitecture.Infrastructure/Extensions/ServiceCollectionExtensions.cs +++ b/CleanArchitecture.Infrastructure/Extensions/ServiceCollectionExtensions.cs @@ -1,20 +1,47 @@ -using CleanArchitecture.Domain.Interfaces; +using CleanArchitecture.Domain.DomainEvents; +using CleanArchitecture.Domain.Interfaces; using CleanArchitecture.Domain.Interfaces.Repositories; using CleanArchitecture.Domain.Notifications; using CleanArchitecture.Infrastructure.Database; +using CleanArchitecture.Infrastructure.EventSourcing; using CleanArchitecture.Infrastructure.Repositories; using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace CleanArchitecture.Infrastructure.Extensions; public static class ServiceCollectionExtensions { - public static IServiceCollection AddInfrastructure(this IServiceCollection services) + public static IServiceCollection AddInfrastructure( + this IServiceCollection services, + IConfiguration configuration, + string migrationsAssemblyName, + string connectionStringName = "DefaultConnection") { + // Add event store db context + services.AddDbContext( + options => + { + options.UseSqlServer( + configuration.GetConnectionString(connectionStringName), + b => b.MigrationsAssembly(migrationsAssemblyName)); + }); + + services.AddDbContext( + options => + { + options.UseSqlServer( + configuration.GetConnectionString(connectionStringName), + b => b.MigrationsAssembly(migrationsAssemblyName)); + }); + // Core Infra services.AddScoped>(); + services.AddScoped(); services.AddScoped, DomainNotificationHandler>(); + services.AddScoped(); services.AddScoped(); // Repositories diff --git a/CleanArchitecture.Infrastructure/InMemoryBus.cs b/CleanArchitecture.Infrastructure/InMemoryBus.cs index 2df646c..0da1dae 100644 --- a/CleanArchitecture.Infrastructure/InMemoryBus.cs +++ b/CleanArchitecture.Infrastructure/InMemoryBus.cs @@ -1,6 +1,6 @@ using System.Threading.Tasks; -using CleanArchitecture.Domain; using CleanArchitecture.Domain.Commands; +using CleanArchitecture.Domain.DomainEvents; using CleanArchitecture.Domain.Interfaces; using MediatR; @@ -9,10 +9,14 @@ namespace CleanArchitecture.Infrastructure; public sealed class InMemoryBus : IMediatorHandler { private readonly IMediator _mediator; + private readonly IDomainEventStore _domainEventStore; - public InMemoryBus(IMediator mediator) + public InMemoryBus( + IMediator mediator, + IDomainEventStore domainEventStore) { _mediator = mediator; + _domainEventStore = domainEventStore; } public Task QueryAsync(IRequest query) @@ -22,7 +26,7 @@ public sealed class InMemoryBus : IMediatorHandler public async Task RaiseEventAsync(T @event) where T : DomainEvent { - // await _domainEventStore.SaveAsync(@event); + await _domainEventStore.SaveAsync(@event); await _mediator.Publish(@event); } diff --git a/CleanArchitecture.Infrastructure/Migrations/DomainNotificationStoreDb/20230701135523_AddDomainNotificationStore.Designer.cs b/CleanArchitecture.Infrastructure/Migrations/DomainNotificationStoreDb/20230701135523_AddDomainNotificationStore.Designer.cs new file mode 100644 index 0000000..23eb179 --- /dev/null +++ b/CleanArchitecture.Infrastructure/Migrations/DomainNotificationStoreDb/20230701135523_AddDomainNotificationStore.Designer.cs @@ -0,0 +1,85 @@ +// +using System; +using CleanArchitecture.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CleanArchitecture.Infrastructure.Migrations.DomainNotificationStoreDb +{ + [DbContext(typeof(DomainNotificationStoreDbContext))] + [Migration("20230701135523_AddDomainNotificationStore")] + partial class AddDomainNotificationStore + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("CleanArchitecture.Domain.DomainNotifications.StoredDomainNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AggregateId") + .HasColumnType("uniqueidentifier"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("SerializedData") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("Data"); + + b.Property("Timestamp") + .HasColumnType("datetime2"); + + b.Property("User") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("Version") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("StoredDomainNotifications"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/CleanArchitecture.Infrastructure/Migrations/DomainNotificationStoreDb/20230701135523_AddDomainNotificationStore.cs b/CleanArchitecture.Infrastructure/Migrations/DomainNotificationStoreDb/20230701135523_AddDomainNotificationStore.cs new file mode 100644 index 0000000..bb09c31 --- /dev/null +++ b/CleanArchitecture.Infrastructure/Migrations/DomainNotificationStoreDb/20230701135523_AddDomainNotificationStore.cs @@ -0,0 +1,43 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CleanArchitecture.Infrastructure.Migrations.DomainNotificationStoreDb +{ + /// + public partial class AddDomainNotificationStore : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "StoredDomainNotifications", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Data = table.Column(type: "nvarchar(max)", nullable: false), + User = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), + CorrelationId = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), + AggregateId = table.Column(type: "uniqueidentifier", nullable: false), + MessageType = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), + Timestamp = table.Column(type: "datetime2", nullable: false), + Key = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), + Value = table.Column(type: "nvarchar(1024)", maxLength: 1024, nullable: false), + Code = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), + Version = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_StoredDomainNotifications", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "StoredDomainNotifications"); + } + } +} diff --git a/CleanArchitecture.Infrastructure/Migrations/DomainNotificationStoreDb/DomainNotificationStoreDbContextModelSnapshot.cs b/CleanArchitecture.Infrastructure/Migrations/DomainNotificationStoreDb/DomainNotificationStoreDbContextModelSnapshot.cs new file mode 100644 index 0000000..ff547bb --- /dev/null +++ b/CleanArchitecture.Infrastructure/Migrations/DomainNotificationStoreDb/DomainNotificationStoreDbContextModelSnapshot.cs @@ -0,0 +1,82 @@ +// +using System; +using CleanArchitecture.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CleanArchitecture.Infrastructure.Migrations.DomainNotificationStoreDb +{ + [DbContext(typeof(DomainNotificationStoreDbContext))] + partial class DomainNotificationStoreDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("CleanArchitecture.Domain.DomainNotifications.StoredDomainNotification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AggregateId") + .HasColumnType("uniqueidentifier"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("MessageType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("SerializedData") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("Data"); + + b.Property("Timestamp") + .HasColumnType("datetime2"); + + b.Property("User") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("Version") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("StoredDomainNotifications"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/CleanArchitecture.Infrastructure/Migrations/EventStoreDb/20230701135441_AddEventStore.Designer.cs b/CleanArchitecture.Infrastructure/Migrations/EventStoreDb/20230701135441_AddEventStore.Designer.cs new file mode 100644 index 0000000..679ba18 --- /dev/null +++ b/CleanArchitecture.Infrastructure/Migrations/EventStoreDb/20230701135441_AddEventStore.Designer.cs @@ -0,0 +1,67 @@ +// +using System; +using CleanArchitecture.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CleanArchitecture.Infrastructure.Migrations.EventStoreDb +{ + [DbContext(typeof(EventStoreDbContext))] + [Migration("20230701135441_AddEventStore")] + partial class AddEventStore + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("CleanArchitecture.Domain.DomainEvents.StoredDomainEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AggregateId") + .HasColumnType("uniqueidentifier"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("MessageType") + .IsRequired() + .HasColumnType("varchar(100)") + .HasColumnName("Action"); + + b.Property("Timestamp") + .HasColumnType("datetime2") + .HasColumnName("CreationDate"); + + b.Property("User") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("Id"); + + b.ToTable("StoredDomainEvents"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/CleanArchitecture.Infrastructure/Migrations/EventStoreDb/20230701135441_AddEventStore.cs b/CleanArchitecture.Infrastructure/Migrations/EventStoreDb/20230701135441_AddEventStore.cs new file mode 100644 index 0000000..b30585f --- /dev/null +++ b/CleanArchitecture.Infrastructure/Migrations/EventStoreDb/20230701135441_AddEventStore.cs @@ -0,0 +1,39 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CleanArchitecture.Infrastructure.Migrations.EventStoreDb +{ + /// + public partial class AddEventStore : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "StoredDomainEvents", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Data = table.Column(type: "nvarchar(max)", nullable: false), + User = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), + CorrelationId = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), + AggregateId = table.Column(type: "uniqueidentifier", nullable: false), + Action = table.Column(type: "varchar(100)", nullable: false), + CreationDate = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_StoredDomainEvents", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "StoredDomainEvents"); + } + } +} diff --git a/CleanArchitecture.Infrastructure/Migrations/EventStoreDb/EventStoreDbContextModelSnapshot.cs b/CleanArchitecture.Infrastructure/Migrations/EventStoreDb/EventStoreDbContextModelSnapshot.cs new file mode 100644 index 0000000..30c362d --- /dev/null +++ b/CleanArchitecture.Infrastructure/Migrations/EventStoreDb/EventStoreDbContextModelSnapshot.cs @@ -0,0 +1,64 @@ +// +using System; +using CleanArchitecture.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CleanArchitecture.Infrastructure.Migrations.EventStoreDb +{ + [DbContext(typeof(EventStoreDbContext))] + partial class EventStoreDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("CleanArchitecture.Domain.DomainEvents.StoredDomainEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AggregateId") + .HasColumnType("uniqueidentifier"); + + b.Property("CorrelationId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("MessageType") + .IsRequired() + .HasColumnType("varchar(100)") + .HasColumnName("Action"); + + b.Property("Timestamp") + .HasColumnType("datetime2") + .HasColumnName("CreationDate"); + + b.Property("User") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("Id"); + + b.ToTable("StoredDomainEvents"); + }); +#pragma warning restore 612, 618 + } + } +}