Skip to content

Commit

Permalink
Add domain events
Browse files Browse the repository at this point in the history
  • Loading branch information
PeterKneale committed Jun 30, 2024
1 parent ee31874 commit 56b2f5b
Show file tree
Hide file tree
Showing 25 changed files with 184 additions and 59 deletions.
8 changes: 3 additions & 5 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
[Do you use Strongly Typed IDs to avoid Primitive Obsession](https://www.ssw.com.au/rules/do-you-use-strongly-typed-ids/)

ValueObjects
Entities
DomainEvents

Table-per-concrete-type (TPC), in which each concrete type in the .NET hierarchy is mapped to a different table in the database, where each table contains columns for all properties of the corresponding type.
https://learn.microsoft.com/en-us/ef/core/performance/modeling-for-performance#inheritance-mapping
Entities, DomainEvents
- https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/domain-events-design-implementation
- https://lostechies.com/jimmybogard/2014/05/13/a-better-domain-events-pattern/
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using Simple.Domain.Tenants;
using Simple.Domain.Users;

namespace Simple.App;
namespace Simple.App.Contracts;

public interface IExecutionContext
{
Expand Down
6 changes: 6 additions & 0 deletions src/Simple.App/Contracts/IDomainEventHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Simple.App.Contracts;

public interface IDomainEventHandler<in T> where T: IDomainEvent
{
Task Handle(T domainEvent);
}
2 changes: 2 additions & 0 deletions src/Simple.App/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Simple.App.Contracts;
using ExecutionContext = Simple.App.Contracts.ExecutionContext;

namespace Simple.App;

Expand Down
4 changes: 0 additions & 4 deletions src/Simple.App/Simple.App.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,4 @@
<ItemGroup>
<ProjectReference Include="..\Simple.Infra\Simple.Infra.csproj" />
</ItemGroup>

<ItemGroup>
<Folder Include="Tenants\" />
</ItemGroup>
</Project>
3 changes: 2 additions & 1 deletion src/Simple.App/Surveys/Commands/CreateSurvey.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Simple.Domain.Surveys;
using Simple.App.Contracts;
using Simple.Domain.Surveys;
using Simple.Domain.Surveys.Specifications;

namespace Simple.App.Surveys.Commands;
Expand Down
13 changes: 13 additions & 0 deletions src/Simple.App/Tenants/DomainEvents/TenantCreatedHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Simple.App.Contracts;
using Simple.Domain.Tenants.DomainEvents;

namespace Simple.App.Tenants.DomainEvents;

public class TenantCreatedHandler(ILogger<TenantCreatedHandler> log) : IDomainEventHandler<TenantCreatedEvent>
{
public Task Handle(TenantCreatedEvent domainEvent)
{
log.LogInformation("Tenant Created: {Tenant}", domainEvent.Tenant);
return Task.CompletedTask;
}
}
15 changes: 8 additions & 7 deletions src/Simple.App/Users/Queries/ListUsers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,23 @@ namespace Simple.App.Users.Queries;

public static class ListUsers
{
public record Query(Guid TenantId, int PageNumber, int PageSize) : IRequest<Results>;

public record Results(IEnumerable<Result> Items);
public record Query(Guid TenantId, int PageNumber, int PageSize) : IRequest<PaginatedResult<Result>>;

public record Result(Guid UserId, string UserName);

public class Validator : AbstractValidator<Query>;

public class Handler(IReadRepository<User> repo) : IRequestHandler<Query, Results>
public class Handler(IReadRepository<User> repo) : IRequestHandler<Query, PaginatedResult<Result>>
{
public async Task<Results> Handle(Query query, CancellationToken cancellationToken)
public async Task<PaginatedResult<Result>> Handle(Query query, CancellationToken cancellationToken)
{
var tenantId = new TenantId(query.TenantId);
var page = new Page(query.PageNumber, query.PageSize);
var results = await repo.ListAsync(new UserListPaginatedSpec(tenantId, page), cancellationToken);
return new Results(results.Select(t => new Result(t.UserId, t.Name.FullName)));
var spec = new UserListPaginatedSpec(tenantId, page);
var list = await repo.ListAsync(spec, cancellationToken);
var count = await repo.CountAsync(spec, cancellationToken);
var items = list.Select(t => new Result(t.UserId, t.Name.FullName)).ToList();
return new PaginatedResult<Result>(items, count, query.PageNumber, query.PageSize);
}
}
}
25 changes: 25 additions & 0 deletions src/Simple.Domain/Entity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace Simple.Domain;

public interface IEntity
{
IEnumerable<IDomainEvent> DomainEvents { get; }
void AddDomainEvent(IDomainEvent eventItem);
void ClearDomainEvents();
}

public abstract class Entity : IEntity
{
private readonly List<IDomainEvent> _domainEvents = [];

public IEnumerable<IDomainEvent> DomainEvents => _domainEvents;

public void AddDomainEvent(IDomainEvent eventItem)
{
_domainEvents.Add(eventItem);
}

public void ClearDomainEvents()
{
_domainEvents.Clear();
}
}
4 changes: 1 addition & 3 deletions src/Simple.Domain/IAggregateRoot.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
namespace Simple.Domain;

public interface IAggregateRoot
{
}
public interface IAggregateRoot;
8 changes: 8 additions & 0 deletions src/Simple.Domain/IDomainEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using MediatR;

namespace Simple.Domain;

/// <summary>
/// https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/domain-events-design-implementation
/// </summary>
public interface IDomainEvent : INotification;
1 change: 1 addition & 0 deletions src/Simple.Domain/Simple.Domain.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<ItemGroup>
<PackageReference Include="Ardalis.Specification" Version="8.0.0" />
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="MediatR.Contracts" Version="2.0.1" />
</ItemGroup>

</Project>
6 changes: 6 additions & 0 deletions src/Simple.Domain/Tenants/DomainEvents/TenantCreatedEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Simple.Domain.Tenants.DomainEvents;

public class TenantCreatedEvent(Tenant tenant) : IDomainEvent
{
public Tenant Tenant { get; } = tenant;
}
7 changes: 5 additions & 2 deletions src/Simple.Domain/Tenants/Tenant.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
namespace Simple.Domain.Tenants;
using Simple.Domain.Tenants.DomainEvents;

public class Tenant : IAggregateRoot
namespace Simple.Domain.Tenants;

public class Tenant : Entity, IAggregateRoot
{
private Tenant()
{
Expand All @@ -12,6 +14,7 @@ public Tenant(TenantId id, TenantName name)
TenantId = id;
TenantName = name;
CreatedAt = SystemTime.UtcNow();
AddDomainEvent(new TenantCreatedEvent(this));
}

public TenantId TenantId { get; private init; } = null!;
Expand Down
17 changes: 10 additions & 7 deletions src/Simple.Infra/Behaviours/LoggingBehaviour.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
using Newtonsoft.Json;
using System.Diagnostics;
using Newtonsoft.Json;

namespace Simple.Infra.Behaviours;

public class LoggingBehaviour<TRequest, TResponse>(ILogger<IMediator> log) : IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
where TResponse : notnull
internal class LoggingBehaviour<TRequest, TResponse>(ILogger<LoggingBehaviour<TRequest, TResponse>> log)
: IPipelineBehavior<TRequest, TResponse> where TRequest : notnull where TResponse : notnull
{

public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{
var name = request.GetType().FullName!.Split(".").Last();
Expand All @@ -29,17 +28,21 @@ private static bool IsQuery(string name) =>

private async Task<TResponse> HandleQuery(RequestHandlerDelegate<TResponse> next, string name, string body)
{
var stopwatch = Stopwatch.StartNew();
log.LogInformation($"Start Query: {name} - {body}");
var result = await next();
log.LogInformation($"End Query {name} - {body}");
var duration = stopwatch.ElapsedMilliseconds;
log.LogDebug($"End Query {name} in {duration}ms - {body}");
return result;
}

private async Task<TResponse> HandleCommand(RequestHandlerDelegate<TResponse> next, string name, string body)
{
var stopwatch = Stopwatch.StartNew();
log.LogInformation($"Start Command: {name} - {body}");
var response = await next();
log.LogInformation($"End Command: {name} - {body}");
var duration = stopwatch.ElapsedMilliseconds;
log.LogDebug($"End Command: {name} in {duration}ms - {body}");
return response;
}
}
12 changes: 6 additions & 6 deletions src/Simple.Infra/Behaviours/TransactionalBehaviour.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
using Simple.Infra.Database;
using Simple.Infra.DomainEvents;

namespace Simple.Infra.Behaviours;

public class TransactionalBehaviour<TRequest, TResponse>(Db db, ILogger<TransactionalBehaviour<TRequest, TResponse>> log)
: IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
where TResponse : notnull
internal class TransactionalBehaviour<TRequest, TResponse>(Db db, IDomainEventDispatcher dispatcher, ILogger<TransactionalBehaviour<TRequest, TResponse>> log)
: IPipelineBehavior<TRequest, TResponse> where TRequest : notnull where TResponse : notnull
{
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{
Expand All @@ -21,12 +20,13 @@ private static bool IsQuery(string name) =>

private async Task<TResponse> HandleCommand(RequestHandlerDelegate<TResponse> next, string name, CancellationToken cancellationToken)
{
log.LogInformation($"Start Transaction: {name}");
log.LogDebug($"Start Transaction: {name}");
await db.Database.BeginTransactionAsync(cancellationToken);
var response = await next();
await dispatcher.Publish();
await db.SaveChangesAsync(cancellationToken);
await db.Database.CommitTransactionAsync(cancellationToken);
log.LogInformation($"End Transaction: {name}");
log.LogDebug($"End Transaction: {name}");
return response;
}
}
5 changes: 2 additions & 3 deletions src/Simple.Infra/Behaviours/ValidationBehaviour.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@

namespace Simple.Infra.Behaviours;

public class ValidationBehaviour<TRequest, TResponse>(IEnumerable<IValidator<TRequest>> validators, ILogger<TRequest> logs) : IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
where TResponse : notnull
internal class ValidationBehaviour<TRequest, TResponse>(IEnumerable<IValidator<TRequest>> validators, ILogger<ValidationBehaviour<TRequest, TResponse>> logs)
: IPipelineBehavior<TRequest, TResponse> where TRequest : notnull where TResponse : notnull
{
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,7 @@ public void Configure(EntityTypeBuilder<Tenant> builder)
.HasMaxLength(NameMaxLength)
.HasColumnName(NameColumn)
.HasConversion(new TenantNameConverter());

builder.Ignore(x => x.DomainEvents);
}
}
31 changes: 31 additions & 0 deletions src/Simple.Infra/DomainEvents/DomainEventDispatcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System.Collections.Immutable;
using Simple.Infra.Database;

namespace Simple.Infra.DomainEvents;

public interface IDomainEventDispatcher
{
Task Publish();
}

internal class DomainEventDispatcher(Db db, IPublisher mediator, ILogger<DomainEventDispatcher> log) : IDomainEventDispatcher
{
public async Task Publish()
{
var entities = db.ChangeTracker.Entries<Entity>()
.Select(po => po.Entity)
.Where(po => po.DomainEvents.Any())
.ToArray();

foreach (var entity in entities)
{
log.LogDebug($"Publishing domain events from {entity.GetType().Name}");
foreach (var domainEvent in entity.DomainEvents)
{
log.LogDebug($"Publishing domain event {domainEvent.GetType().Name}");
await mediator.Publish(domainEvent);
}
entity.ClearDomainEvents();
}
}
}
4 changes: 4 additions & 0 deletions src/Simple.Infra/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Simple.Infra.Behaviours;
using Simple.Infra.Database;
using Simple.Infra.Database.Repositories;
using Simple.Infra.DomainEvents;
using MigrationRunner = Simple.Infra.Database.Migrations.MigrationRunner;

namespace Simple.Infra;
Expand Down Expand Up @@ -45,6 +46,9 @@ public static IServiceCollection AddInfra(this IServiceCollection services, ICon
// repositories
services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
services.AddScoped(typeof(IReadRepository<>), typeof(ReadRepository<>));

// ddd
services.AddScoped<IDomainEventDispatcher, DomainEventDispatcher>();
return services;
}
}
1 change: 1 addition & 0 deletions tests/Simple.IntegrationTests/BaseTest.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using MediatR;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
using Simple.App.Contracts;
using Simple.Domain.Tenants;
using Simple.Domain.Users;

Expand Down
13 changes: 10 additions & 3 deletions tests/Simple.IntegrationTests/Fixtures/ServiceFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,15 @@ public ServiceFixture()
.AddInfra(_config)
.AddLogging(builder => builder.AddXUnit(this, c =>
{
c.Filter = (category, level) => true;
builder.SetMinimumLevel(LogLevel.Debug);
c.Filter = (category, level) =>
{
if (category.StartsWith("Simple"))

Check warning on line 29 in tests/Simple.IntegrationTests/Fixtures/ServiceFixture.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.
return level >= LogLevel.Information;
if (category.StartsWith("Microsoft.EntityFrameworkCore"))
return level >= LogLevel.Warning;
return level >= LogLevel.Warning;
};
}))
.BuildServiceProvider();
ResetDatabase();
Expand All @@ -35,7 +43,7 @@ public void ResetDatabase()
}

public IServiceProvider ServiceProvider => _provider;

public IConfiguration Configuration => _config;

public void Dispose()
Expand All @@ -44,5 +52,4 @@ public void Dispose()
}

public ITestOutputHelper? OutputHelper { get; set; }

}
Loading

0 comments on commit 56b2f5b

Please sign in to comment.