Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,19 @@
</ItemGroup>
<ItemGroup Label="TargetFramework .NET 8" Condition="'$(TargetFramework)' == 'net8.0'">
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="8.0.25" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.25" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.25" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.25" />
</ItemGroup>
<ItemGroup Label="TargetFramework .NET 9" Condition="'$(TargetFramework)' == 'net9.0'">
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="9.0.14" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.14" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.14" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.14" />
</ItemGroup>
<ItemGroup Label="TargetFramework .NET 10" Condition="'$(TargetFramework)' == 'net10.0'">
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.5" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.5" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.5" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.5" />
</ItemGroup>
Expand Down
2 changes: 2 additions & 0 deletions Pulse.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@
<Folder Name="/tests/">
<Project Path="tests/NetEvolve.Pulse.EntityFramework.Tests.Integration/NetEvolve.Pulse.EntityFramework.Tests.Integration.csproj" />
<Project Path="tests/NetEvolve.Pulse.SqlServer.Tests.Integration/NetEvolve.Pulse.SqlServer.Tests.Integration.csproj" />
<Project Path="tests/NetEvolve.Pulse.EntityFramework.Tests.Unit/NetEvolve.Pulse.EntityFramework.Tests.Unit.csproj" />
<Project Path="tests/NetEvolve.Pulse.Polly.Tests.Integration/NetEvolve.Pulse.Polly.Tests.Integration.csproj" />
<Project Path="tests/NetEvolve.Pulse.Polly.Tests.Unit/NetEvolve.Pulse.Polly.Tests.Unit.csproj" />
<Project Path="tests/NetEvolve.Pulse.SqlServer.Tests.Unit/NetEvolve.Pulse.SqlServer.Tests.Unit.csproj" />
<Project Path="tests/NetEvolve.Pulse.Tests.Integration/NetEvolve.Pulse.Tests.Integration.csproj" />
<Project Path="tests/NetEvolve.Pulse.Tests.Unit/NetEvolve.Pulse.Tests.Unit.csproj" />
</Folder>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
namespace NetEvolve.Pulse.EntityFramework.Tests.Unit;

using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using NetEvolve.Pulse;
using NetEvolve.Pulse.Extensibility;
using NetEvolve.Pulse.Outbox;
using TUnit.Core;

public sealed class EntityFrameworkEventOutboxTests
{
[Test]
public async Task Constructor_WithNullContext_ThrowsArgumentNullException() =>
_ = await Assert
.That(() =>
new EntityFrameworkEventOutbox<TestDbContext>(
null!,
Options.Create(new OutboxOptions()),
TimeProvider.System
)
)
.Throws<ArgumentNullException>();

[Test]
public async Task Constructor_WithNullOptions_ThrowsArgumentNullException()
{
var options = new DbContextOptionsBuilder<TestDbContext>()
.UseInMemoryDatabase(nameof(Constructor_WithNullOptions_ThrowsArgumentNullException))
.Options;
await using var context = new TestDbContext(options);

_ = await Assert
.That(() => new EntityFrameworkEventOutbox<TestDbContext>(context, null!, TimeProvider.System))
.Throws<ArgumentNullException>();
}

[Test]
public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException()
{
var options = new DbContextOptionsBuilder<TestDbContext>()
.UseInMemoryDatabase(nameof(Constructor_WithNullTimeProvider_ThrowsArgumentNullException))
.Options;
await using var context = new TestDbContext(options);

_ = await Assert
.That(() =>
new EntityFrameworkEventOutbox<TestDbContext>(context, Options.Create(new OutboxOptions()), null!)
)
.Throws<ArgumentNullException>();
}

[Test]
public async Task Constructor_WithValidArguments_CreatesInstance()
{
var options = new DbContextOptionsBuilder<TestDbContext>()
.UseInMemoryDatabase(nameof(Constructor_WithValidArguments_CreatesInstance))
.Options;
await using var context = new TestDbContext(options);

var outbox = new EntityFrameworkEventOutbox<TestDbContext>(
context,
Options.Create(new OutboxOptions()),
TimeProvider.System
);

_ = await Assert.That(outbox).IsNotNull();
}

[Test]
public async Task StoreAsync_WithNullMessage_ThrowsArgumentNullException()
{
var options = new DbContextOptionsBuilder<TestDbContext>()
.UseInMemoryDatabase(nameof(StoreAsync_WithNullMessage_ThrowsArgumentNullException))
.Options;
await using var context = new TestDbContext(options);
var outbox = new EntityFrameworkEventOutbox<TestDbContext>(
context,
Options.Create(new OutboxOptions()),
TimeProvider.System
);

_ = await Assert
.That(async () => await outbox.StoreAsync<TestEvent>(null!).ConfigureAwait(false))
.Throws<ArgumentNullException>();
}

[Test]
public async Task StoreAsync_WithLongCorrelationId_ThrowsInvalidOperationException()
{
var options = new DbContextOptionsBuilder<TestDbContext>()
.UseInMemoryDatabase(nameof(StoreAsync_WithLongCorrelationId_ThrowsInvalidOperationException))
.Options;
await using var context = new TestDbContext(options);
var outbox = new EntityFrameworkEventOutbox<TestDbContext>(
context,
Options.Create(new OutboxOptions()),
TimeProvider.System
);
var message = new TestEvent
{
CorrelationId = new string('x', OutboxMessageSchema.MaxLengths.CorrelationId + 1),
};

_ = await Assert
.That(async () => await outbox.StoreAsync(message).ConfigureAwait(false))
.Throws<InvalidOperationException>();
}

private sealed record TestEvent : IEvent
{
public string? CorrelationId { get; set; }
public string Id { get; init; } = Guid.NewGuid().ToString();
public DateTimeOffset? PublishedAt { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
namespace NetEvolve.Pulse.EntityFramework.Tests.Unit;

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using NetEvolve.Pulse;
using NetEvolve.Pulse.Extensibility;
using NetEvolve.Pulse.Outbox;
using TUnit.Core;

public sealed class EntityFrameworkMediatorConfiguratorExtensionsTests
{
[Test]
public async Task AddEntityFrameworkOutbox_WithNullConfigurator_ThrowsArgumentNullException() =>
_ = await Assert
.That(() => EntityFrameworkMediatorConfiguratorExtensions.AddEntityFrameworkOutbox<TestDbContext>(null!))
.Throws<ArgumentNullException>();

[Test]
public async Task AddEntityFrameworkOutbox_WithValidConfigurator_ReturnsConfiguratorForChaining()
{
var stub = new MediatorConfiguratorStub();

var result = stub.AddEntityFrameworkOutbox<TestDbContext>();

_ = await Assert.That(result).IsSameReferenceAs(stub);
}

[Test]
public async Task AddEntityFrameworkOutbox_RegistersOutboxRepositoryAsScoped()
{
var services = new ServiceCollection();
_ = services.AddDbContext<TestDbContext>(o =>
o.UseInMemoryDatabase(nameof(AddEntityFrameworkOutbox_RegistersOutboxRepositoryAsScoped))
);
_ = services.AddPulse(config => config.AddOutbox().AddEntityFrameworkOutbox<TestDbContext>());

var descriptor = services.FirstOrDefault(d => d.ServiceType == typeof(IOutboxRepository));

using (Assert.Multiple())
{
_ = await Assert.That(descriptor).IsNotNull();
_ = await Assert.That(descriptor!.Lifetime).IsEqualTo(ServiceLifetime.Scoped);
}
}

[Test]
public async Task AddEntityFrameworkOutbox_RegistersEventOutboxAsScoped()
{
var services = new ServiceCollection();
_ = services.AddDbContext<TestDbContext>(o =>
o.UseInMemoryDatabase(nameof(AddEntityFrameworkOutbox_RegistersEventOutboxAsScoped))
);
_ = services.AddPulse(config => config.AddOutbox().AddEntityFrameworkOutbox<TestDbContext>());

var descriptor = services.FirstOrDefault(d => d.ServiceType == typeof(IEventOutbox));

using (Assert.Multiple())
{
_ = await Assert.That(descriptor).IsNotNull();
_ = await Assert.That(descriptor!.Lifetime).IsEqualTo(ServiceLifetime.Scoped);
}
}

[Test]
public async Task AddEntityFrameworkOutbox_RegistersTransactionScopeAsScoped()
{
var services = new ServiceCollection();
_ = services.AddDbContext<TestDbContext>(o =>
o.UseInMemoryDatabase(nameof(AddEntityFrameworkOutbox_RegistersTransactionScopeAsScoped))
);
_ = services.AddPulse(config => config.AddOutbox().AddEntityFrameworkOutbox<TestDbContext>());

var descriptor = services.FirstOrDefault(d => d.ServiceType == typeof(IOutboxTransactionScope));

using (Assert.Multiple())
{
_ = await Assert.That(descriptor).IsNotNull();
_ = await Assert.That(descriptor!.Lifetime).IsEqualTo(ServiceLifetime.Scoped);
}
}

[Test]
public async Task AddEntityFrameworkOutbox_RegistersTimeProviderAsSingleton()
{
var services = new ServiceCollection();
_ = services.AddPulse(config => config.AddEntityFrameworkOutbox<TestDbContext>());

var descriptor = services.FirstOrDefault(d => d.ServiceType == typeof(TimeProvider));

using (Assert.Multiple())
{
_ = await Assert.That(descriptor).IsNotNull();
_ = await Assert.That(descriptor!.Lifetime).IsEqualTo(ServiceLifetime.Singleton);
}
}

[Test]
public async Task AddEntityFrameworkOutbox_WithConfigureOptions_AppliesOptions()
{
var services = new ServiceCollection();
_ = services.AddDbContext<TestDbContext>(o =>
o.UseInMemoryDatabase(nameof(AddEntityFrameworkOutbox_WithConfigureOptions_AppliesOptions))
);
_ = services.AddPulse(config =>
config.AddOutbox().AddEntityFrameworkOutbox<TestDbContext>(options => options.Schema = "myschema")
);

await using var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<OutboxOptions>>();

_ = await Assert.That(options.Value.Schema).IsEqualTo("myschema");
}

[Test]
public async Task AddEntityFrameworkOutbox_WithTableNameOption_AppliesOptions()
{
var services = new ServiceCollection();
_ = services.AddDbContext<TestDbContext>(o =>
o.UseInMemoryDatabase(nameof(AddEntityFrameworkOutbox_WithTableNameOption_AppliesOptions))
);
_ = services.AddPulse(config =>
config.AddOutbox().AddEntityFrameworkOutbox<TestDbContext>(options => options.TableName = "CustomOutbox")
);

await using var provider = services.BuildServiceProvider();
var options = provider.GetRequiredService<IOptions<OutboxOptions>>();

_ = await Assert.That(options.Value.TableName).IsEqualTo("CustomOutbox");
}

private sealed class MediatorConfiguratorStub : IMediatorConfigurator
{
public IServiceCollection Services { get; } = new ServiceCollection();

public IMediatorConfigurator AddActivityAndMetrics() => throw new NotImplementedException();

public IMediatorConfigurator UseDefaultEventDispatcher<TDispatcher>(
ServiceLifetime lifetime = ServiceLifetime.Singleton
)
where TDispatcher : class, IEventDispatcher => throw new NotImplementedException();

public IMediatorConfigurator UseDefaultEventDispatcher<TDispatcher>(
Func<IServiceProvider, TDispatcher> factory,
ServiceLifetime lifetime = ServiceLifetime.Singleton
)
where TDispatcher : class, IEventDispatcher => throw new NotImplementedException();

public IMediatorConfigurator UseEventDispatcherFor<TEvent, TDispatcher>(
ServiceLifetime lifetime = ServiceLifetime.Singleton
)
where TEvent : IEvent
where TDispatcher : class, IEventDispatcher => throw new NotImplementedException();

public IMediatorConfigurator UseEventDispatcherFor<TEvent, TDispatcher>(
Func<IServiceProvider, TDispatcher> factory,
ServiceLifetime lifetime = ServiceLifetime.Singleton
)
where TEvent : IEvent
where TDispatcher : class, IEventDispatcher => throw new NotImplementedException();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
namespace NetEvolve.Pulse.EntityFramework.Tests.Unit;

using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using NetEvolve.Pulse;
using TUnit.Core;

public sealed class EntityFrameworkOutboxRepositoryTests
{
[Test]
public async Task Constructor_WithNullContext_ThrowsArgumentNullException() =>
_ = await Assert
.That(() => new EntityFrameworkOutboxRepository<TestDbContext>(null!, TimeProvider.System))
.Throws<ArgumentNullException>();

[Test]
public async Task Constructor_WithNullTimeProvider_ThrowsArgumentNullException()
{
var options = new DbContextOptionsBuilder<TestDbContext>()
.UseInMemoryDatabase(nameof(Constructor_WithNullTimeProvider_ThrowsArgumentNullException))
.Options;
await using var context = new TestDbContext(options);

_ = await Assert
.That(() => new EntityFrameworkOutboxRepository<TestDbContext>(context, null!))
.Throws<ArgumentNullException>();
}

[Test]
public async Task Constructor_WithValidArguments_CreatesInstance()
{
var options = new DbContextOptionsBuilder<TestDbContext>()
.UseInMemoryDatabase(nameof(Constructor_WithValidArguments_CreatesInstance))
.Options;
await using var context = new TestDbContext(options);

var repository = new EntityFrameworkOutboxRepository<TestDbContext>(context, TimeProvider.System);

_ = await Assert.That(repository).IsNotNull();
}

[Test]
public async Task AddAsync_WithNullMessage_ThrowsArgumentNullException()
{
var options = new DbContextOptionsBuilder<TestDbContext>()
.UseInMemoryDatabase(nameof(AddAsync_WithNullMessage_ThrowsArgumentNullException))
.Options;
await using var context = new TestDbContext(options);
var repository = new EntityFrameworkOutboxRepository<TestDbContext>(context, TimeProvider.System);

_ = await Assert
.That(async () => await repository.AddAsync(null!).ConfigureAwait(false))
.Throws<ArgumentNullException>();
}
}
Loading
Loading