Skip to content

Commit

Permalink
Improve domain event publisher implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
incompetent-developer committed Nov 20, 2023
1 parent b090ff8 commit e0b9c2e
Show file tree
Hide file tree
Showing 5 changed files with 270 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace Codehard.Infrastructure.EntityFramework.Tests;
public class DomainEntityTests
{
[Fact]
public async Task WhenSaveChanges_UsingDomainEventDbContext_ShouldPublishAndClearEvents()
public async Task WhenSaveChangesAsync_UsingDomainEventDbContext_ShouldPublishAndClearEvents()
{
// Arrange
var assembly = Assembly.GetExecutingAssembly();
Expand Down Expand Up @@ -59,7 +59,7 @@ public async Task WhenSaveChanges_UsingDomainEventDbContext_ShouldPublishAndClea
}

[Fact]
public async Task WhenSaveChanges_UsingGlobalPublisherFunction_ShouldPublishAndClearEvents()
public async Task WhenSaveChangesAsync_UsingGlobalPublisherFunction_ShouldPublishAndClearEvents()
{
// Arrange
var assembly = Assembly.GetExecutingAssembly();
Expand Down Expand Up @@ -113,6 +113,109 @@ public async Task WhenSaveChanges_UsingGlobalPublisherFunction_ShouldPublishAndC
Assert.Empty(entity.Events);
}

[Fact]
public void WhenSaveChanges_UsingDomainEventDbContext_ShouldPublishAndClearEvents()
{
// Arrange
var assembly = Assembly.GetExecutingAssembly();
var options = new DbContextOptionsBuilder<TestDbContext>()
.UseSqlite(CreateInMemoryDatabase())
.Options;

var loggerMock = new Mock<ILogger<TestDbContext>>();
var logger = loggerMock.Object;
using var context = new TestDbContext(
options,
builder => builder.ApplyConfigurationsFromAssemblyFor<TestDbContext>(assembly),
logger);
context.Database.EnsureCreated();

// Act
var entity = EntityA.Create();
entity.UpdateValue("New Value");

context.As.Add(entity);
context.SaveChanges();

// Assert
loggerMock.Verify(
logger =>
logger.Log(
It.Is<LogLevel>(logLevel => logLevel == LogLevel.Information),
It.Is<EventId>(eventId => true),
It.Is<It.IsAnyType>((@object, @type) =>
@object.ToString()!.Contains(nameof(EntityCreatedEvent))),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
Times.Once);
loggerMock.Verify(
logger =>
logger.Log(
It.Is<LogLevel>(logLevel => logLevel == LogLevel.Information),
It.Is<EventId>(eventId => true),
It.Is<It.IsAnyType>((@object, @type) =>
@object.ToString()!.Contains(nameof(ValueChangedEvent))),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
Times.Once);
Assert.Empty(entity.Events);
}

[Fact]
public void WhenSaveChanges_UsingGlobalPublisherFunction_ShouldPublishAndClearEvents()
{
// Arrange
var assembly = Assembly.GetExecutingAssembly();
var options = new DbContextOptionsBuilder<TestDbContext>()
.UseSqlite(CreateInMemoryDatabase())
.Options;

var loggerMock = new Mock<ILogger<TestDbContext>>();
var logger = loggerMock.Object;
using var context = new TestDbContext(
options,
builder => builder.ApplyConfigurationsFromAssemblyFor<TestDbContext>(assembly),
dm =>
{
// We use LogWarning here to distinct
// between the global and local (within the DbContext) publisher
logger.LogWarning(dm.ToString());

return Task.CompletedTask;
});
context.Database.EnsureCreated();

// Act
var entity = EntityA.Create();
entity.UpdateValue("New Value");

context.As.Add(entity);
context.SaveChanges();

// Assert
loggerMock.Verify(
logger =>
logger.Log(
It.Is<LogLevel>(logLevel => logLevel == LogLevel.Warning),
It.Is<EventId>(eventId => true),
It.Is<It.IsAnyType>((@object, @type) =>
@object.ToString()!.Contains(nameof(EntityCreatedEvent))),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
Times.Once);
loggerMock.Verify(
logger =>
logger.Log(
It.Is<LogLevel>(logLevel => logLevel == LogLevel.Warning),
It.Is<EventId>(eventId => true),
It.Is<It.IsAnyType>((@object, @type) =>
@object.ToString()!.Contains(nameof(ValueChangedEvent))),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
Times.Once);
Assert.Empty(entity.Events);
}

private static SqliteConnection CreateInMemoryDatabase()
{
var connection = new SqliteConnection("DataSource=:memory:");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public class EntityCreatedEvent : IDomainEvent<EntityAKey>
public record ValueChangedEvent(EntityAKey Id, string NewValue)
: IDomainEvent<EntityAKey>;

public class EntityAKey : IEntityKey
public struct EntityAKey
{
public Guid Value { get; set; }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>3.0.0-preview-2</Version>
<Version>3.0.0-preview-3</Version>
<Description>A library contains common code related to Entity Framework Core.</Description>
<PackageProjectUrl>https://github.com/codehardth/Codehard.Common</PackageProjectUrl>
<RepositoryUrl>https://github.com/codehardth/Codehard.Common</RepositoryUrl>
<PackageReleaseNotes>Added a foundation for domain event.</PackageReleaseNotes>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup>

<ItemGroup>
Expand All @@ -23,11 +24,11 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Codehard.Common\Codehard.Common.DomainModel\Codehard.Common.DomainModel.csproj" />
<InternalsVisibleTo Include="Codehard.Infrastructure.EntityFramework.Tests"/>
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="Codehard.Infrastructure.EntityFramework.Tests"/>
<ProjectReference Include="..\..\Codehard.Common\Codehard.Common.DomainModel\Codehard.Common.DomainModel.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System.Collections.Concurrent;
using System.Linq.Expressions;

namespace Codehard.Infrastructure.EntityFramework;

internal class InstanceActivatorFactory
{
private readonly ConcurrentDictionary<Type, Func<object>> cache = new();

private static InstanceActivatorFactory? instance;

public static readonly InstanceActivatorFactory Instance = instance ??= new InstanceActivatorFactory();

private InstanceActivatorFactory()
{
}

public object CreateInstance(Type type)
{
if (this.cache.TryGetValue(type, out var activator))
{
return activator();
}

var expression = Expression.Lambda<Func<object>>(Expression.New(type));
var compiledExpression = expression.Compile();

this.cache.AddOrUpdate(type, _ => compiledExpression, (_, _) => compiledExpression);

return compiledExpression();
}

public object CreateInstance<T>()
{
return this.CreateInstance(typeof(T));
}
}
Loading

0 comments on commit e0b9c2e

Please sign in to comment.