Skip to content

Commit 03f4903

Browse files
committed
Fix: One Scope per seeder, instead of one scope per group
1 parent 8c27c43 commit 03f4903

File tree

4 files changed

+74
-71
lines changed

4 files changed

+74
-71
lines changed

src/CodeOfChaos.Types.DataSeeder/ISeeder.cs

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
// ---------------------------------------------------------------------------------------------------------------------
22
// Imports
33
// ---------------------------------------------------------------------------------------------------------------------
4-
using Microsoft.Extensions.Logging;
5-
64
namespace CodeOfChaos.Types;
75
// ---------------------------------------------------------------------------------------------------------------------
86
// Code
@@ -12,21 +10,29 @@ namespace CodeOfChaos.Types;
1210
/// and executes the seeding logic if required.
1311
/// </summary>
1412
public interface ISeeder {
13+
bool ShouldSeed { get; }
14+
15+
// -----------------------------------------------------------------------------------------------------------------
16+
// Methods
17+
// -----------------------------------------------------------------------------------------------------------------
1518
/// <summary>
16-
/// Initiates the seeding process for the implementing class.
19+
/// Initiates the seeding process for the implementing class.
1720
/// </summary>
18-
/// <param name="logger">An instance of <see cref="ILogger" /> for logging operations.</param>
21+
/// <param name="serviceProvider">
22+
/// An instance of <see cref="IServiceProvider" /> used to resolve dependencies during the seeding process.
23+
/// </param>
1924
/// <param name="ct">
20-
/// A <see cref="CancellationToken" /> to observe while waiting for the task to complete. Defaults to
21-
/// <see cref="CancellationToken.None" />.
25+
/// A <see cref="CancellationToken" /> to observe while waiting for the task to complete. Defaults to
26+
/// <see cref="CancellationToken.None" />.
2227
/// </param>
2328
/// <returns>A <see cref="Task" /> that represents the asynchronous operation.</returns>
2429
/// <remarks>
25-
/// This method should first checks whether seeding should occur by calling <c>ShouldSeedAsync</c>. If seeding is not
26-
/// needed, it logs an informational message and returns.
27-
/// If seeding is required, the method executes <c>SeedAsync</c>, respecting the provided cancellation token.
30+
/// This method orchestrates the seeding process, ensuring any required dependencies are resolved via the provided
31+
/// <c>serviceProvider</c>. If the implementation detects that seeding is unnecessary,
32+
/// it will gracefully conclude the operation. If any steps in the seeding process require cancellation,
33+
/// they will respect the provided <c>CancellationToken</c>.
2834
/// </remarks>
29-
Task StartAsync(ILogger logger, CancellationToken ct = default);
35+
Task StartAsync(IServiceProvider serviceProvider, CancellationToken ct = default);
3036

3137
/// Determines whether the seeding process should proceed or be skipped.
3238
/// <param name="ct">A cancellation token that can be used to cancel the operation.</param>

src/CodeOfChaos.Types.DataSeeder/OneTimeDataSeederService.cs

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -69,19 +69,27 @@ public async Task StartAsync(CancellationToken ct = default) {
6969
continue;
7070
}
7171

72-
// Each group should have their own scope
73-
await using AsyncServiceScope scope = serviceProvider.CreateAsyncScope();
74-
IServiceProvider scopeProvider = scope.ServiceProvider;
75-
List<ISeeder> seeders = [];
72+
// Each SEEDER should have their own scope
73+
List<(Task, AsyncServiceScope)> seederTasks = [];
7674

7775
while (seederGroup.SeederTypes.TryDequeue(out Type? seederType)) {
78-
var seeder = (ISeeder)scopeProvider.GetRequiredService(seederType);
79-
seeders.Add(seeder);
80-
76+
AsyncServiceScope scope = serviceProvider.CreateAsyncScope();
77+
IServiceProvider scopeProvider = scope.ServiceProvider;
78+
// Because of checks by the SeederGroup struct we know that the seeder inherits from ISeeder and thus is not null
79+
var seeder = (ISeeder) scopeProvider.GetRequiredService(seederType);
80+
Task seederTask = seeder.StartAsync(scopeProvider, ct);
81+
82+
// Because our scope has to be gracefully disposed, we add the scope here
83+
seederTasks.Add((seederTask, scope));
8184
}
8285

83-
logger.LogDebug("ExecutionStep {step} : {count} Seeder(s) found, executing...", i++, seeders.Count);
84-
await Task.WhenAll(seeders.Select(seeder => seeder.StartAsync(logger, ct)));
86+
logger.LogDebug("ExecutionStep {step} : {count} Seeder(s) found, executing...", i++, seederTasks.Count);
87+
await Task.WhenAll(seederTasks.Select(t => t.Item1));
88+
89+
// Gracefully dispose the scope
90+
foreach (AsyncServiceScope scope in seederTasks.Select(t => t.Item2)) {
91+
await scope.DisposeAsync();
92+
}
8593
}
8694

8795
logger.LogInformation("All seeders completed in {i} steps", i);

src/CodeOfChaos.Types.DataSeeder/Seeder.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// ---------------------------------------------------------------------------------------------------------------------
22
// Imports
33
// ---------------------------------------------------------------------------------------------------------------------
4+
using Microsoft.Extensions.DependencyInjection;
45
using Microsoft.Extensions.Logging;
56

67
namespace CodeOfChaos.Types;
@@ -12,10 +13,14 @@ namespace CodeOfChaos.Types;
1213
/// a base class for seeding operations with pre-seeding validation logic.
1314
/// </summary>
1415
public abstract class Seeder : ISeeder {
16+
public bool ShouldSeed { get; private set; } = false;
17+
1518
/// <inheritdoc />
16-
public async Task StartAsync(ILogger logger, CancellationToken ct = default) {
17-
if (!await ShouldSeedAsync(ct)) {
18-
logger.LogInformation("Skipping seeding");
19+
public async Task StartAsync(IServiceProvider serviceProvider, CancellationToken ct = default) {
20+
ShouldSeed = await ShouldSeedAsync(ct);
21+
if (!ShouldSeed) {
22+
var logger = serviceProvider.GetService<ILogger>();
23+
logger?.LogInformation("Skipping seeding");
1924
return;
2025
}
2126

tests/Tests.CodeOfChaos.Types.DataSeeder/SeederTests.cs

Lines changed: 33 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -11,64 +11,48 @@ namespace Tests.CodeOfChaos.Types.DataSeeder;
1111
// ---------------------------------------------------------------------------------------------------------------------
1212
public class SeederTests {
1313
[Test]
14-
public async Task StartAsync_ShouldLogAndReturn_WhenShouldSeedReturnsFalse() {
15-
// Arrange
16-
var loggerMock = new Mock<ILogger>();
17-
var seeder = new TestSeeder {
18-
ShouldSeedResult = false// Simulate ShouldSeedAsync returning false
19-
};
14+
public async Task StartAsync_ShouldLogAndReturn_WhenShouldSeedReturnsFalse() {
15+
// Arrange
16+
var serviceProviderMock = new Mock<IServiceProvider>();
17+
var seeder = new TestSeeder {
18+
ShouldSeedResult = false // Simulate ShouldSeedAsync returning false
19+
};
2020

21-
// Act
22-
await seeder.StartAsync(loggerMock.Object);
21+
// Act
22+
await seeder.StartAsync(serviceProviderMock.Object);
2323

24-
// Assert
25-
loggerMock.Verify(
26-
expression: logger => logger.Log(
27-
LogLevel.Information,
28-
It.IsAny<EventId>(),
29-
It.Is<It.IsAnyType>((state, _) => state.ToString()!.Contains("Skipping seeding")),
30-
It.IsAny<Exception>(),
31-
It.IsAny<Func<It.IsAnyType, Exception, string>>()!),
32-
Times.Once);
33-
await Assert.That(seeder.SeedWasCalled).IsFalse();
34-
}
24+
// Assert
25+
await Assert.That(seeder.ShouldSeed).IsFalse();
26+
}
3527

3628
[Test]
37-
public async Task StartAsync_ShouldCallSeed_WhenShouldSeedReturnsTrue() {
38-
// Arrange
39-
var loggerMock = new Mock<ILogger>();
40-
var seeder = new TestSeeder {
41-
ShouldSeedResult = true// Simulate ShouldSeedAsync returning true
42-
};
29+
public async Task StartAsync_ShouldCallSeed_WhenShouldSeedReturnsTrue() {
30+
// Arrange
31+
var serviceProviderMock = new Mock<IServiceProvider>();
32+
var seeder = new TestSeeder {
33+
ShouldSeedResult = true // Simulate ShouldSeedAsync returning true
34+
};
4335

44-
// Act
45-
await seeder.StartAsync(loggerMock.Object);
36+
// Act
37+
await seeder.StartAsync(serviceProviderMock.Object);
4638

47-
// Assert
48-
loggerMock.Verify(
49-
expression: logger => logger.Log(
50-
It.IsAny<LogLevel>(),
51-
It.IsAny<EventId>(),
52-
It.IsAny<It.IsAnyType>(),
53-
It.IsAny<Exception>(),
54-
It.IsAny<Func<It.IsAnyType, Exception, string>>()!),
55-
Times.Never);// No additional logging should occur during SeedAsync
56-
await Assert.That(seeder.SeedWasCalled).IsTrue();
57-
}
39+
// Assert
40+
await Assert.That(seeder.ShouldSeed).IsTrue();
41+
}
5842

5943
[Test]
60-
public async Task StartAsync_ShouldRespectCancellationToken() {
61-
// Arrange
62-
var loggerMock = new Mock<ILogger>();
63-
var seeder = new TestSeeder();
64-
using var cts = new CancellationTokenSource();
65-
await cts.CancelAsync();// Simulate cancellation
44+
public async Task StartAsync_ShouldRespectCancellationToken() {
45+
// Arrange
46+
var serviceProviderMock = new Mock<IServiceProvider>();
47+
var seeder = new TestSeeder();
48+
using var cts = new CancellationTokenSource();
49+
await cts.CancelAsync(); // Simulate cancellation
6650

67-
// Act & Assert
68-
await Assert.ThrowsAsync<OperationCanceledException>(async () => {
69-
await seeder.StartAsync(loggerMock.Object, cts.Token);
70-
});
71-
}
51+
// Act & Assert
52+
await Assert.ThrowsAsync<OperationCanceledException>(async () => {
53+
await seeder.StartAsync(serviceProviderMock.Object, cts.Token);
54+
});
55+
}
7256

7357
// Test implementation of Seeder for unit tests
7458
public class TestSeeder : Seeder {

0 commit comments

Comments
 (0)