mirror of
https://github.com/alex289/CleanArchitecture.git
synced 2025-06-30 02:31:08 +00:00
Add event sourcing
This commit is contained in:
parent
745b3930f0
commit
53e0966f51
@ -40,7 +40,7 @@ builder.Services.AddDbContext<ApplicationDbContext>(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<ApplicationDbContext>();
|
||||
var storeDbContext = services.GetRequiredService<EventStoreDbContext>();
|
||||
var domainStoreDbContext = services.GetRequiredService<DomainNotificationStoreDbContext>();
|
||||
|
||||
appDbContext.EnsureMigrationsApplied();
|
||||
storeDbContext.EnsureMigrationsApplied();
|
||||
domainStoreDbContext.EnsureMigrationsApplied();
|
||||
}
|
||||
|
||||
app.Run();
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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<UserUpdatedEvent>()
|
||||
.VerifyAnyDomainNotification()
|
||||
.VerifyExistingNotification(
|
||||
DomainErrorCodes.UserAlreadyExists,
|
||||
$"There is already a User with Email {command.Email}");
|
||||
}
|
||||
}
|
@ -21,7 +21,7 @@ public sealed class UpdateUserCommandTestFixture : CommandHandlerFixtureBase
|
||||
}
|
||||
|
||||
public UpdateUserCommandHandler CommandHandler { get; }
|
||||
private Mock<IUserRepository> UserRepository { get; }
|
||||
public Mock<IUserRepository> UserRepository { get; }
|
||||
|
||||
public Entities.User SetupUser()
|
||||
{
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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; }
|
||||
}
|
19
CleanArchitecture.Domain/DomainEvents/DomainEvent.cs
Normal file
19
CleanArchitecture.Domain/DomainEvents/DomainEvent.cs
Normal file
@ -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; }
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CleanArchitecture.Domain.DomainEvents;
|
||||
|
||||
public interface IDomainEventStore
|
||||
{
|
||||
Task SaveAsync<T>(T domainEvent) where T : DomainEvent;
|
||||
}
|
22
CleanArchitecture.Domain/DomainEvents/Message.cs
Normal file
22
CleanArchitecture.Domain/DomainEvents/Message.cs
Normal file
@ -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; }
|
||||
}
|
28
CleanArchitecture.Domain/DomainEvents/StoredDomainEvent.cs
Normal file
28
CleanArchitecture.Domain/DomainEvents/StoredDomainEvent.cs
Normal file
@ -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())
|
||||
{ }
|
||||
}
|
@ -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)
|
||||
{ }
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using CleanArchitecture.Domain.DomainEvents;
|
||||
|
||||
namespace CleanArchitecture.Domain.Events.User;
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using CleanArchitecture.Domain.DomainEvents;
|
||||
|
||||
namespace CleanArchitecture.Domain.Events.User;
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using CleanArchitecture.Domain.DomainEvents;
|
||||
|
||||
namespace CleanArchitecture.Domain.Events.User;
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using CleanArchitecture.Domain.DomainEvents;
|
||||
|
||||
namespace CleanArchitecture.Domain.Events.User;
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System.Threading.Tasks;
|
||||
using CleanArchitecture.Domain.Commands;
|
||||
using CleanArchitecture.Domain.DomainEvents;
|
||||
using MediatR;
|
||||
|
||||
namespace CleanArchitecture.Domain.Interfaces;
|
||||
|
@ -8,4 +8,5 @@ public interface IUser
|
||||
string Name { get; }
|
||||
Guid GetUserId();
|
||||
UserRole GetUserRole();
|
||||
string GetUserEmail();
|
||||
}
|
@ -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;
|
||||
}
|
@ -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<IMediator>();
|
||||
var domainEventStore = new Mock<IDomainEventStore>();
|
||||
|
||||
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<IMediator>();
|
||||
var domainEventStore = new Mock<IDomainEventStore>();
|
||||
|
||||
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<IMediator>();
|
||||
var domainEventStore = new Mock<IDomainEventStore>();
|
||||
|
||||
var inMemoryBus = new InMemoryBus(mediator.Object);
|
||||
var inMemoryBus = new InMemoryBus(mediator.Object, domainEventStore.Object);
|
||||
|
||||
var deleteUserCommand = new DeleteUserCommand(Guid.NewGuid());
|
||||
|
||||
|
@ -18,6 +18,7 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -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<StoredDomainEvent>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<StoredDomainEvent> 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)");
|
||||
}
|
||||
}
|
@ -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<StoredDomainNotification>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<StoredDomainNotification> 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");
|
||||
}
|
||||
}
|
@ -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<DomainNotificationStoreDbContext> options) : base(options)
|
||||
{
|
||||
}
|
||||
|
||||
public virtual DbSet<StoredDomainNotification> StoredDomainNotifications { get; set; } = null!;
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
modelBuilder.ApplyConfiguration(new StoredDomainNotificationConfiguration());
|
||||
}
|
||||
}
|
@ -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<EventStoreDbContext> options) : base(options)
|
||||
{
|
||||
}
|
||||
|
||||
public virtual DbSet<StoredDomainEvent> StoredDomainEvents { get; set; } = null!;
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.ApplyConfiguration(new StoredDomainEventConfiguration());
|
||||
|
||||
base.OnModelCreating(modelBuilder);
|
||||
}
|
||||
}
|
56
CleanArchitecture.Infrastructure/EventSourcing/EventStore.cs
Normal file
56
CleanArchitecture.Infrastructure/EventSourcing/EventStore.cs
Normal file
@ -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>(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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
namespace CleanArchitecture.Infrastructure.EventSourcing;
|
||||
|
||||
public interface IEventStoreContext
|
||||
{
|
||||
public string GetUserEmail();
|
||||
public string GetCorrelationId();
|
||||
}
|
@ -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<EventStoreDbContext>(
|
||||
options =>
|
||||
{
|
||||
options.UseSqlServer(
|
||||
configuration.GetConnectionString(connectionStringName),
|
||||
b => b.MigrationsAssembly(migrationsAssemblyName));
|
||||
});
|
||||
|
||||
services.AddDbContext<DomainNotificationStoreDbContext>(
|
||||
options =>
|
||||
{
|
||||
options.UseSqlServer(
|
||||
configuration.GetConnectionString(connectionStringName),
|
||||
b => b.MigrationsAssembly(migrationsAssemblyName));
|
||||
});
|
||||
|
||||
// Core Infra
|
||||
services.AddScoped<IUnitOfWork, UnitOfWork<ApplicationDbContext>>();
|
||||
services.AddScoped<IEventStoreContext, EventStoreContext>();
|
||||
services.AddScoped<INotificationHandler<DomainNotification>, DomainNotificationHandler>();
|
||||
services.AddScoped<IDomainEventStore, DomainEventStore>();
|
||||
services.AddScoped<IMediatorHandler, InMemoryBus>();
|
||||
|
||||
// Repositories
|
||||
|
@ -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<TResponse> QueryAsync<TResponse>(IRequest<TResponse> query)
|
||||
@ -22,7 +26,7 @@ public sealed class InMemoryBus : IMediatorHandler
|
||||
|
||||
public async Task RaiseEventAsync<T>(T @event) where T : DomainEvent
|
||||
{
|
||||
// await _domainEventStore.SaveAsync(@event);
|
||||
await _domainEventStore.SaveAsync(@event);
|
||||
|
||||
await _mediator.Publish(@event);
|
||||
}
|
||||
|
@ -0,0 +1,85 @@
|
||||
// <auto-generated />
|
||||
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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("AggregateId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("CorrelationId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("MessageType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("SerializedData")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.HasColumnName("Data");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("User")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("nvarchar(1024)");
|
||||
|
||||
b.Property<int>("Version")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("StoredDomainNotifications");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace CleanArchitecture.Infrastructure.Migrations.DomainNotificationStoreDb
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddDomainNotificationStore : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "StoredDomainNotifications",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
Data = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
User = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||
CorrelationId = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||
AggregateId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
MessageType = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||
Timestamp = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
Key = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||
Value = table.Column<string>(type: "nvarchar(1024)", maxLength: 1024, nullable: false),
|
||||
Code = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||
Version = table.Column<int>(type: "int", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_StoredDomainNotifications", x => x.Id);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "StoredDomainNotifications");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
// <auto-generated />
|
||||
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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("AggregateId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("CorrelationId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("MessageType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("SerializedData")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)")
|
||||
.HasColumnName("Data");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("User")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("nvarchar(1024)");
|
||||
|
||||
b.Property<int>("Version")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("StoredDomainNotifications");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
67
CleanArchitecture.Infrastructure/Migrations/EventStoreDb/20230701135441_AddEventStore.Designer.cs
generated
Normal file
67
CleanArchitecture.Infrastructure/Migrations/EventStoreDb/20230701135441_AddEventStore.Designer.cs
generated
Normal file
@ -0,0 +1,67 @@
|
||||
// <auto-generated />
|
||||
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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("AggregateId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("CorrelationId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("MessageType")
|
||||
.IsRequired()
|
||||
.HasColumnType("varchar(100)")
|
||||
.HasColumnName("Action");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("CreationDate");
|
||||
|
||||
b.Property<string>("User")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("StoredDomainEvents");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace CleanArchitecture.Infrastructure.Migrations.EventStoreDb
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddEventStore : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "StoredDomainEvents",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
Data = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
User = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||
CorrelationId = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||
AggregateId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
Action = table.Column<string>(type: "varchar(100)", nullable: false),
|
||||
CreationDate = table.Column<DateTime>(type: "datetime2", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_StoredDomainEvents", x => x.Id);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "StoredDomainEvents");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
// <auto-generated />
|
||||
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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("AggregateId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("CorrelationId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("Data")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("MessageType")
|
||||
.IsRequired()
|
||||
.HasColumnType("varchar(100)")
|
||||
.HasColumnName("Action");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("datetime2")
|
||||
.HasColumnName("CreationDate");
|
||||
|
||||
b.Property<string>("User")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("StoredDomainEvents");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user