Skip to content

Commit 9ba82b8

Browse files
committed
Feat: Service Provider Scope per group
1 parent bb8015b commit 9ba82b8

File tree

9 files changed

+332
-220
lines changed

9 files changed

+332
-220
lines changed

src/CodeOfChaos.Types.DataSeeder/IDataSeeder.cs

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,56 @@ namespace CodeOfChaos.Types;
99
// Code
1010
// ---------------------------------------------------------------------------------------------------------------------
1111
public interface IDataSeederService : IHostedService {
12+
/// <summary>
13+
/// Adds a seeder of type TSeeder to the data seeder service.
14+
/// TSeeder must implement the ISeeder interface.
15+
/// </summary>
16+
/// <typeparam name="TSeeder">The type of the seeder to be added.</typeparam>
17+
/// <returns>An instance of IDataSeederService to allow method chaining.</returns>
1218
IDataSeederService AddSeeder<TSeeder>() where TSeeder : ISeeder;
13-
IDataSeederService AddSeederGroup(params ISeeder[] seeders);
19+
20+
/// <summary>
21+
/// Adds a group of seeders to the seeder service using the specified <paramref name="group"/> configuration callback.
22+
/// </summary>
23+
/// <param name="group">
24+
/// An action that allows configuration of the <see cref="SeederGroup"/> by adding individual seeders.
25+
/// </param>
26+
/// <returns>
27+
/// The updated instance of <see cref="IDataSeederService"/> to allow method chaining.
28+
/// </returns>
1429
IDataSeederService AddSeederGroup(Action<SeederGroup> group);
30+
31+
/// <summary>
32+
/// Adds a group of seeders to the current seeder service.
33+
/// </summary>
34+
/// <param name="group">
35+
/// An instance of <see cref="SeederGroup"/> containing the seeders to be added.
36+
/// </param>
37+
/// <returns>
38+
/// The current instance of <see cref="IDataSeederService"/> with the added seeder group.
39+
/// </returns>
1540
IDataSeederService AddSeederGroup(SeederGroup group);
16-
41+
42+
/// <summary>
43+
/// Adds remaining seeder types from the provided assembly to the data seeder service.
44+
/// Ensures that only non-abstract, non-generic, and non-interface types implementing the <see cref="ISeeder"/> interface are added.
45+
/// </summary>
46+
/// <param name="assembly">
47+
/// The assembly from which seeder types will be collected.
48+
/// </param>
49+
/// <exception cref="AggregateException">
50+
/// Thrown when one or more seeder types fail to be instantiated.
51+
/// </exception>
1752
void AddRemainderSeeders(Assembly assembly);
53+
54+
/// <summary>
55+
/// Adds all unprocessed data seeder types from the provided assembly as a single group to be executed together.
56+
/// </summary>
57+
/// <param name="assembly">
58+
/// The assembly from which to collect remaining data seeder types.
59+
/// </param>
60+
/// <exception cref="AggregateException">
61+
/// Thrown if there are multiple errors while adding seeders to the group.
62+
/// </exception>
1863
void AddRemainderSeedersAsOneGroup(Assembly assembly);
1964
}

src/CodeOfChaos.Types.DataSeeder/ISeeder.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,34 @@ namespace CodeOfChaos.Types;
77
// ---------------------------------------------------------------------------------------------------------------------
88
// Code
99
// ---------------------------------------------------------------------------------------------------------------------
10+
/// <summary>
11+
/// Defines a contract for a seeding mechanism that determines whether a seed process should run
12+
/// and executes the seeding logic if required.
13+
/// </summary>
1014
public interface ISeeder {
15+
/// <summary>
16+
/// Initiates the seeding process for the implementing class.
17+
/// </summary>
18+
/// <param name="logger">An instance of <see cref="ILogger"/> for logging operations.</param>
19+
/// <param name="ct">A <see cref="CancellationToken"/> to observe while waiting for the task to complete. Defaults to <see cref="CancellationToken.None"/>.</param>
20+
/// <returns>A <see cref="Task"/> that represents the asynchronous operation.</returns>
21+
/// <remarks>
22+
/// This method should first checks whether seeding should occur by calling <c>ShouldSeedAsync</c>. If seeding is not needed, it logs an informational message and returns.
23+
/// If seeding is required, the method executes <c>SeedAsync</c>, respecting the provided cancellation token.
24+
/// </remarks>
1125
Task StartAsync(ILogger logger, CancellationToken ct = default);
26+
27+
/// Determines whether the seeding process should proceed or be skipped.
28+
/// <param name="ct">A cancellation token that can be used to cancel the operation.</param>
29+
/// <return>A task representing the asynchronous operation. The task result contains a boolean indicating
30+
/// whether the seeding process should proceed (true) or be skipped (false).</return>
1231
Task<bool> ShouldSeedAsync(CancellationToken ct = default);
32+
33+
/// <summary>
34+
/// Executes the seed operation for a data seeder. This method is intended to be overridden
35+
/// by derived classes to implement data seeding logic.
36+
/// </summary>
37+
/// <param name="ct">The <see cref="CancellationToken"/> that can be used to signal a request to cancel the operation.</param>
38+
/// <returns>A task that represents the asynchronous operation.</returns>
1339
Task SeedAsync(CancellationToken ct = default);
1440
}

src/CodeOfChaos.Types.DataSeeder/OneTimeDataSeederService.cs

Lines changed: 85 additions & 33 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
using System.Collections.Concurrent;
67
using System.Reflection;
@@ -9,19 +10,46 @@ namespace CodeOfChaos.Types;
910
// ---------------------------------------------------------------------------------------------------------------------
1011
// Code
1112
// ---------------------------------------------------------------------------------------------------------------------
12-
public abstract class OneTimeDataSeederService(IServiceProvider serviceProvider, ILogger<OneTimeDataSeederService> logger) : IDataSeederService {
13-
private readonly ConcurrentQueue<SeederGroup> _seeders = [];
14-
private readonly ConcurrentBag<Type> _seederTypes = [];
15-
private bool _collectedRemainders;// If set to true, the remainder seeders have been collected and thus should throw an exception if any are added
13+
public class OneTimeDataSeederService(IServiceProvider serviceProvider, ILogger<OneTimeDataSeederService> logger) : IDataSeederService {
14+
/// <summary>
15+
/// Represents a thread-safe queue of SeederGroup objects used within the OneTimeDataSeederService
16+
/// to maintain and manage seeding operations.
17+
/// </summary>
18+
/// <remarks>
19+
/// The <c>Seeders</c> field serves as the central storage for SeederGroups, allowing them to be
20+
/// enqueued and dequeued in a controlled manner during the seeding process. This ensures
21+
/// proper execution order and thread safety for concurrent operations.
22+
/// </remarks>
23+
protected readonly ConcurrentQueue<SeederGroup> Seeders = [];
24+
25+
/// <summary>
26+
/// Represents a thread-safe collection of seeder types used by the OneTimeDataSeederService to track
27+
/// registered data seeders. This collection ensures that each type of seeder is only added once
28+
/// and prevents duplicates during the seeding process. It plays a critical role in managing
29+
/// and validating the lifecycle of seeders added to the service.
30+
/// </summary>
31+
protected readonly ConcurrentBag<Type> SeederTypes = [];
32+
33+
/// <summary>
34+
/// Indicates whether the remainder seeders have been successfully collected.
35+
/// If set to <c>true</c>, it indicates that no additional remainder seeders can
36+
/// be added to the service, and attempting to do so will throw an exception.
37+
/// </summary>
38+
protected bool CollectedRemainders;// If set to true, the remainder seeders have been collected and thus should throw an exception if any are added
1639

1740
// -----------------------------------------------------------------------------------------------------------------
1841
// Methods
1942
// -----------------------------------------------------------------------------------------------------------------
43+
/// <summary>
44+
/// Starts the data seeding process by executing all configured seeder groups in sequence.
45+
/// Validates seeders, collects data, and manages the lifecycle of each group using scoped services.
46+
/// </summary>
47+
/// <param name="ct">A CancellationToken used to observe cancellation requests.</param>
48+
/// <returns>A Task representing the asynchronous operation.</returns>
2049
public async Task StartAsync(CancellationToken ct = default) {
2150
logger.LogInformation("DataSeederService starting...");
22-
23-
// User has to collect the seeders
24-
await CollectAsync(ct);
51+
52+
await CollectAsync(ct); // If user choose the old format of adding seeders, this will be what ads the seeders to the queue
2553
ct.ThrowIfCancellationRequested();// Don't throw during collection, but throw afterward
2654

2755
// Validation has to succeed before we continue
@@ -30,7 +58,7 @@ public async Task StartAsync(CancellationToken ct = default) {
3058
if (!ValidateSeeders()) return;
3159

3260
int i = 0;
33-
foreach (SeederGroup seederGroup in _seeders) {
61+
while (Seeders.TryDequeue(out SeederGroup seederGroup)) {
3462
if (ct.IsCancellationRequested) {
3563
logger.LogWarning("Seeding process canceled during execution.");
3664
break;
@@ -41,54 +69,79 @@ public async Task StartAsync(CancellationToken ct = default) {
4169
continue;
4270
}
4371

44-
logger.LogDebug("ExecutionStep {step} : {count} Seeder(s) found, executing...", i++, seederGroup.Count);
45-
await Task.WhenAll(seederGroup.Select(seeder => seeder.StartAsync(logger, ct)));
72+
// Each group should have their own scope
73+
using IServiceScope scope = serviceProvider.CreateScope();
74+
IServiceProvider scopeProvider = scope.ServiceProvider;
75+
List<ISeeder> seeders = [];
76+
77+
while (seederGroup.SeederTypes.TryDequeue(out Type? seederType)) {
78+
var seeder = (ISeeder)scopeProvider.GetRequiredService(seederType);
79+
seeders.Add(seeder);
80+
81+
}
82+
83+
logger.LogDebug("ExecutionStep {step} : {count} Seeder(s) found, executing...", i++, seeders.Count);
84+
await Task.WhenAll(seeders.Select(seeder => seeder.StartAsync(logger, ct)));
4685
}
4786

4887
logger.LogInformation("All seeders completed in {i} steps", i);
4988
// Cleanup
50-
_seeders.Clear();
51-
_seederTypes.Clear();
52-
_collectedRemainders = false;
89+
Seeders.Clear();
90+
SeederTypes.Clear();
91+
CollectedRemainders = false;
5392
}
5493

94+
/// <summary>
95+
/// Asynchronously stops the OneTimeDataSeederService, handling any necessary cleanup or finalization logic.
96+
/// </summary>
97+
/// <param name="ct">The cancellation token to observe while awaiting the task to complete.</param>
98+
/// <returns>A task that completes when the service has been stopped.</returns>
5599
public Task StopAsync(CancellationToken ct = default) {
56100
logger.LogInformation("Stopping DataSeederService...");
57101
return Task.CompletedTask;
58102
}
59103

104+
/// <summary>
105+
/// Collects seeders asynchronously to prepare for execution based on the seeding logic.
106+
/// This method is intended to be overridden for custom collection logic.
107+
/// </summary>
108+
/// <param name="ct">A <see cref="CancellationToken"/> to observe while waiting for the task to complete.</param>
109+
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
110+
protected virtual Task CollectAsync(CancellationToken ct = default) => Task.CompletedTask;
111+
60112
// -----------------------------------------------------------------------------------------------------------------
61113
// Seeder manipulation Methods
62114
// -----------------------------------------------------------------------------------------------------------------
115+
/// <inheritdoc />
63116
public IDataSeederService AddSeeder<TSeeder>() where TSeeder : ISeeder
64117
=> AddSeederGroup(group => group.AddSeeder<TSeeder>());
65118

66-
public IDataSeederService AddSeederGroup(params ISeeder[] seeders)
67-
=> AddSeederGroup(new SeederGroup(seeders, serviceProvider));
68-
119+
/// <inheritdoc />
69120
public IDataSeederService AddSeederGroup(Action<SeederGroup> group) {
70-
var seeders = new SeederGroup(serviceProvider);
121+
var seeders = new SeederGroup();
71122
group(seeders);
72123
return AddSeederGroup(seeders);
73124
}
74-
125+
126+
/// <inheritdoc />
75127
public IDataSeederService AddSeederGroup(SeederGroup group) {
76128
ThrowIfRemainderSeeders();
77129

78-
_seeders.Enqueue(group);
79-
foreach (ISeeder seeder in group) {
80-
_seederTypes.Add(seeder.GetType());
130+
Seeders.Enqueue(group);
131+
foreach (Type seeder in group.SeederTypes.ToArray()) {
132+
SeederTypes.Add(seeder);
81133
}
82134

83135
return this;
84136
}
85-
137+
138+
/// <inheritdoc />
86139
public void AddRemainderSeeders(Assembly assembly) {
87140
Type[] types = CollectTypes(assembly);
88141
var errors = new List<Exception>();
89142

90143
foreach (Type type in types) {
91-
if (_seederTypes.Contains(type)) {
144+
if (SeederTypes.Contains(type)) {
92145
logger.LogDebug("Skipping {t} as it was already assigned", type);
93146
continue;
94147
}
@@ -104,23 +157,24 @@ public void AddRemainderSeeders(Assembly assembly) {
104157

105158
if (errors.Count != 0) throw new AggregateException(errors);
106159

107-
_collectedRemainders = true;
160+
CollectedRemainders = true;
108161
}
109-
162+
163+
/// <inheritdoc />
110164
public void AddRemainderSeedersAsOneGroup(Assembly assembly) {
111165
Type[] types = CollectTypes(assembly);
112-
var group = new SeederGroup(serviceProvider);
166+
var group = new SeederGroup();
113167
var errors = new List<Exception>();
114168

115169
foreach (Type type in types) {
116-
if (_seederTypes.Contains(type)) {
170+
if (SeederTypes.Contains(type)) {
117171
logger.LogDebug("Skipping {t} as it was already assigned", type);
118172
continue;
119173
}
120174

121175
try {
122176
group.AddSeeder(type);
123-
_seederTypes.Add(type);
177+
SeederTypes.Add(type);
124178
}
125179
catch (Exception ex) {
126180
logger.LogError(ex, "Failed to instantiate {t}. Skipping...", type);
@@ -132,11 +186,9 @@ public void AddRemainderSeedersAsOneGroup(Assembly assembly) {
132186

133187
// Collect as one Concurrent step
134188
AddSeederGroup(group);
135-
_collectedRemainders = true;
189+
CollectedRemainders = true;
136190
}
137191

138-
protected virtual Task CollectAsync(CancellationToken ct = default) => Task.CompletedTask;
139-
140192
private static Type[] CollectTypes(Assembly assembly)
141193
=> assembly.GetTypes()
142194
// order is deterministic
@@ -146,14 +198,14 @@ private static Type[] CollectTypes(Assembly assembly)
146198
.ToArray();
147199

148200
private void ThrowIfRemainderSeeders() {
149-
if (!_collectedRemainders) return;
201+
if (!CollectedRemainders) return;
150202

151203
logger.LogError("Remainder seeders have already been collected");
152204
throw new InvalidOperationException("Remainder seeders have already been collected");
153205
}
154206

155207
protected virtual bool ValidateSeeders() {
156-
if (!_seeders.IsEmpty) return true;
208+
if (!Seeders.IsEmpty) return true;
157209

158210
logger.LogWarning("No seeders were added prior to execution.");
159211
return false;

src/CodeOfChaos.Types.DataSeeder/Seeder.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ namespace CodeOfChaos.Types;
77
// ---------------------------------------------------------------------------------------------------------------------
88
// Code
99
// ---------------------------------------------------------------------------------------------------------------------
10+
/// <summary>
11+
/// Represents an abstract implementation of the <see cref="ISeeder"/> interface, providing
12+
/// a base class for seeding operations with pre-seeding validation logic.
13+
/// </summary>
1014
public abstract class Seeder : ISeeder {
15+
/// <inheritdoc />
1116
public async Task StartAsync(ILogger logger, CancellationToken ct = default) {
1217
if (!await ShouldSeedAsync(ct)) {
1318
logger.LogInformation("Skipping seeding");
@@ -18,6 +23,9 @@ public async Task StartAsync(ILogger logger, CancellationToken ct = default) {
1823
await SeedAsync(ct);
1924
}
2025

26+
/// <inheritdoc />
2127
public virtual Task<bool> ShouldSeedAsync(CancellationToken ct = default) => Task.FromResult(true);
28+
29+
/// <inheritdoc />
2230
public abstract Task SeedAsync(CancellationToken ct = default);
2331
}

0 commit comments

Comments
 (0)