Skip to content

Commit 4e28bf9

Browse files
committed
Feat: OneTimeDataSeeder
1 parent b9dc7ec commit 4e28bf9

File tree

13 files changed

+676
-0
lines changed

13 files changed

+676
-0
lines changed

CodeOfChaos.Types.sln

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodeOfChaos.Types.TypedValu
1616
EndProject
1717
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.CodeOfChaos.Types.TypedValueStore", "tests\Tests.CodeOfChaos.Types.TypedValueStore\Tests.CodeOfChaos.Types.TypedValueStore.csproj", "{D98BE3BB-BDDB-416A-A9E4-8DA232D0D6A1}"
1818
EndProject
19+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodeOfChaos.Types.DataSeeder", "src\CodeOfChaos.Types.DataSeeder\CodeOfChaos.Types.DataSeeder.csproj", "{5087A810-E729-4D6F-8CC1-4969D6B1F282}"
20+
EndProject
21+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.CodeOfChaos.Types.DataSeeder", "tests\Tests.CodeOfChaos.Types.DataSeeder\Tests.CodeOfChaos.Types.DataSeeder.csproj", "{228BA72E-139E-4C32-AC14-1B415921CE18}"
22+
EndProject
1923
Global
2024
GlobalSection(SolutionConfigurationPlatforms) = preSolution
2125
Debug|Any CPU = Debug|Any CPU
@@ -42,12 +46,22 @@ Global
4246
{D98BE3BB-BDDB-416A-A9E4-8DA232D0D6A1}.Debug|Any CPU.Build.0 = Debug|Any CPU
4347
{D98BE3BB-BDDB-416A-A9E4-8DA232D0D6A1}.Release|Any CPU.ActiveCfg = Release|Any CPU
4448
{D98BE3BB-BDDB-416A-A9E4-8DA232D0D6A1}.Release|Any CPU.Build.0 = Release|Any CPU
49+
{5087A810-E729-4D6F-8CC1-4969D6B1F282}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
50+
{5087A810-E729-4D6F-8CC1-4969D6B1F282}.Debug|Any CPU.Build.0 = Debug|Any CPU
51+
{5087A810-E729-4D6F-8CC1-4969D6B1F282}.Release|Any CPU.ActiveCfg = Release|Any CPU
52+
{5087A810-E729-4D6F-8CC1-4969D6B1F282}.Release|Any CPU.Build.0 = Release|Any CPU
53+
{228BA72E-139E-4C32-AC14-1B415921CE18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
54+
{228BA72E-139E-4C32-AC14-1B415921CE18}.Debug|Any CPU.Build.0 = Debug|Any CPU
55+
{228BA72E-139E-4C32-AC14-1B415921CE18}.Release|Any CPU.ActiveCfg = Release|Any CPU
56+
{228BA72E-139E-4C32-AC14-1B415921CE18}.Release|Any CPU.Build.0 = Release|Any CPU
4557
EndGlobalSection
4658
GlobalSection(NestedProjects) = preSolution
4759
{26284571-0E09-4BAF-8C2B-DF87DCC1BA0B} = {8DD280D4-1E14-4D5E-AFE6-58DD8F079DCC}
4860
{64B26DED-68C3-47FF-B409-1C8FAD4F9176} = {197E72AD-DEAB-4350-AFC3-A3BB38720BF5}
4961
{ADEADD97-0AFA-4D9E-970B-9FFB932949B3} = {AF1A203C-6EF1-440E-BB3C-55B1DBFE9C19}
5062
{0526265F-4F2C-4993-AA75-ABD97C0E9BAF} = {197E72AD-DEAB-4350-AFC3-A3BB38720BF5}
5163
{D98BE3BB-BDDB-416A-A9E4-8DA232D0D6A1} = {8DD280D4-1E14-4D5E-AFE6-58DD8F079DCC}
64+
{5087A810-E729-4D6F-8CC1-4969D6B1F282} = {197E72AD-DEAB-4350-AFC3-A3BB38720BF5}
65+
{228BA72E-139E-4C32-AC14-1B415921CE18} = {8DD280D4-1E14-4D5E-AFE6-58DD8F079DCC}
5266
EndGlobalSection
5367
EndGlobal
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net9.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<RootNamespace>CodeOfChaos.Types</RootNamespace>
8+
9+
<!-- Main package name -->
10+
<PackageId>CodeOfChaos.Types.DataSeeder</PackageId>
11+
<Version>0.3.0</Version>
12+
<Authors>Anna Sas</Authors>
13+
<Description>A small library housing DataSeeder typings</Description>
14+
<PackageProjectUrl>https://github.com/code-of-chaos/cs-code_of_chaos-types</PackageProjectUrl>
15+
<PackageTags>typing</PackageTags>
16+
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
17+
<PublishRepositoryUrl>true</PublishRepositoryUrl>
18+
<IncludeSymbols>true</IncludeSymbols>
19+
<DebugType>embedded</DebugType>
20+
<PackageLicenseFile>LICENSE</PackageLicenseFile>
21+
<PackageReadmeFile>README.md</PackageReadmeFile>
22+
<PackageIcon>icon.png</PackageIcon>
23+
</PropertyGroup>
24+
25+
<ItemGroup Label="InternalsVisibleTo">
26+
<InternalsVisibleTo Include="Tests.CodeOfChaos.Types.DataSeeder"/>
27+
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
28+
</ItemGroup>
29+
30+
<ItemGroup>
31+
<None Include="../../LICENSE" Pack="true" PackagePath="" Visible="false"/>
32+
<None Include="../../README.md" Pack="true" PackagePath="" Visible="false"/>
33+
<None Include="../../assets/icon.png" Pack="true" PackagePath="" Visible="false"/>
34+
</ItemGroup>
35+
36+
<ItemGroup>
37+
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
38+
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.0" />
39+
</ItemGroup>
40+
41+
</Project>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// ---------------------------------------------------------------------------------------------------------------------
2+
// Imports
3+
// ---------------------------------------------------------------------------------------------------------------------
4+
using Microsoft.Extensions.Hosting;
5+
using System.Reflection;
6+
7+
namespace CodeOfChaos.Types;
8+
9+
// ---------------------------------------------------------------------------------------------------------------------
10+
// Code
11+
// ---------------------------------------------------------------------------------------------------------------------
12+
public interface IDataSeederService : IHostedService {
13+
IDataSeederService AddSeeder<TSeeder>() where TSeeder : ISeeder;
14+
IDataSeederService AddSeederGroup(params ISeeder[] seeders);
15+
IDataSeederService AddSeederGroup(Action<SeederGroup> group);
16+
IDataSeederService AddSeederGroup(SeederGroup group);
17+
18+
void AddRemainderSeeders(Assembly assembly);
19+
void AddRemainderSeedersAsOneGroup(Assembly assembly);
20+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// ---------------------------------------------------------------------------------------------------------------------
2+
// Imports
3+
// ---------------------------------------------------------------------------------------------------------------------
4+
using Microsoft.Extensions.Logging;
5+
6+
namespace CodeOfChaos.Types;
7+
8+
// ---------------------------------------------------------------------------------------------------------------------
9+
// Code
10+
// ---------------------------------------------------------------------------------------------------------------------
11+
public interface ISeeder {
12+
Task StartAsync(ILogger logger, CancellationToken ct = default);
13+
Task<bool> ShouldSeedAsync(CancellationToken ct = default);
14+
Task SeedAsync(CancellationToken ct = default);
15+
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
// ---------------------------------------------------------------------------------------------------------------------
2+
// Imports
3+
// ---------------------------------------------------------------------------------------------------------------------
4+
using Microsoft.Extensions.DependencyInjection;
5+
using Microsoft.Extensions.Logging;
6+
using System.Collections.Concurrent;
7+
using System.Reflection;
8+
9+
namespace CodeOfChaos.Types;
10+
// ---------------------------------------------------------------------------------------------------------------------
11+
// Code
12+
// ---------------------------------------------------------------------------------------------------------------------
13+
public abstract class OneTimeDataSeederService(IServiceProvider serviceProvider, ILogger<OneTimeDataSeederService> logger) : IDataSeederService {
14+
private readonly ConcurrentQueue<SeederGroup> _seeders = [];
15+
private readonly ConcurrentBag<Type> _seederTypes = [];
16+
private bool _collectedRemainders;// If set to true, the remainder seeders have been collected and thus should throw an exception if any are added
17+
18+
// -----------------------------------------------------------------------------------------------------------------
19+
// Methods
20+
// -----------------------------------------------------------------------------------------------------------------
21+
public async Task StartAsync(CancellationToken ct = default) {
22+
logger.LogInformation("DataSeederService starting...");
23+
24+
// User has to collect the seeders
25+
await CollectAsync(ct);
26+
ct.ThrowIfCancellationRequested();// Don't throw during collection, but throw afterward
27+
28+
// Validation has to succeed before we continue
29+
// Technically this library doesn't need to validate much,
30+
// but to make sure we've enabled overloading of the method
31+
if (!ValidateSeeders()) return;
32+
33+
int i = 0;
34+
foreach (SeederGroup seederGroup in _seeders) {
35+
if (ct.IsCancellationRequested) {
36+
logger.LogWarning("Seeding process canceled during execution.");
37+
break;
38+
}
39+
40+
if (seederGroup.IsEmpty) {
41+
logger.LogDebug("ExecutionStep {step} : Skipping empty seeder array", i++);
42+
continue;
43+
}
44+
45+
logger.LogDebug("ExecutionStep {step} : {count} Seeder(s) found, executing...", i++, seederGroup.Count);
46+
await Task.WhenAll(seederGroup.Select(seeder => seeder.StartAsync(logger, ct)));
47+
}
48+
49+
logger.LogInformation("All seeders completed in {i} steps", i);
50+
// Cleanup
51+
_seeders.Clear();
52+
_seederTypes.Clear();
53+
_collectedRemainders = false;
54+
}
55+
56+
public Task StopAsync(CancellationToken ct = default) {
57+
logger.LogInformation("Stopping DataSeederService...");
58+
return Task.CompletedTask;
59+
}
60+
61+
protected virtual Task CollectAsync(CancellationToken ct = default) => Task.CompletedTask;
62+
63+
// -----------------------------------------------------------------------------------------------------------------
64+
// Seeder manipulation Methods
65+
// -----------------------------------------------------------------------------------------------------------------
66+
public IDataSeederService AddSeeder<TSeeder>() where TSeeder : ISeeder
67+
=> AddSeederGroup(group => group.AddSeeder<TSeeder>());
68+
69+
public IDataSeederService AddSeederGroup(params ISeeder[] seeders)
70+
=> AddSeederGroup(new SeederGroup(seeders, serviceProvider));
71+
72+
public IDataSeederService AddSeederGroup(Action<SeederGroup> group) {
73+
var seeders = new SeederGroup(serviceProvider);
74+
group(seeders);
75+
return AddSeederGroup(seeders);
76+
}
77+
78+
public IDataSeederService AddSeederGroup(SeederGroup group) {
79+
ThrowIfRemainderSeeders();
80+
81+
_seeders.Enqueue(group);
82+
foreach (ISeeder seeder in group) {
83+
_seederTypes.Add(seeder.GetType());
84+
}
85+
86+
return this;
87+
}
88+
89+
public void AddRemainderSeeders(Assembly assembly) {
90+
Type[] types = CollectTypes(assembly);
91+
var errors = new List<Exception>();
92+
93+
foreach (Type type in types) {
94+
if (_seederTypes.Contains(type)) {
95+
logger.LogDebug("Skipping {t} as it was already assigned", type);
96+
continue;
97+
}
98+
99+
try {
100+
AddSeederGroup(group => group.AddSeeder(type));
101+
}
102+
catch (Exception ex) {
103+
logger.LogError(ex, "Failed to instantiate {t}. Skipping...", type);
104+
errors.Add(ex);
105+
}
106+
}
107+
108+
if (errors.Count != 0) throw new AggregateException(errors);
109+
110+
_collectedRemainders = true;
111+
}
112+
113+
public void AddRemainderSeedersAsOneGroup(Assembly assembly) {
114+
Type[] types = CollectTypes(assembly);
115+
var group = new SeederGroup(serviceProvider);
116+
var errors = new List<Exception>();
117+
118+
foreach (Type type in types) {
119+
if (_seederTypes.Contains(type)) {
120+
logger.LogDebug("Skipping {t} as it was already assigned", type);
121+
continue;
122+
}
123+
124+
try {
125+
group.AddSeeder(type);
126+
_seederTypes.Add(type);
127+
}
128+
catch (Exception ex) {
129+
logger.LogError(ex, "Failed to instantiate {t}. Skipping...", type);
130+
errors.Add(ex);
131+
}
132+
}
133+
134+
if (errors.Count != 0) throw new AggregateException(errors);
135+
136+
// Collect as one Concurrent step
137+
AddSeederGroup(group);
138+
_collectedRemainders = true;
139+
}
140+
141+
private static Type[] CollectTypes(Assembly assembly)
142+
=> assembly.GetTypes()
143+
// order is deterministic
144+
.OrderBy(t => t.FullName)
145+
.Where(type => type.IsAssignableTo(typeof(ISeeder))
146+
&& type is { IsAbstract: false, IsInterface: false, IsGenericTypeDefinition: false })
147+
.ToArray();
148+
149+
private void ThrowIfRemainderSeeders() {
150+
if (!_collectedRemainders) return;
151+
152+
logger.LogError("Remainder seeders have already been collected");
153+
throw new InvalidOperationException("Remainder seeders have already been collected");
154+
}
155+
156+
protected virtual bool ValidateSeeders() {
157+
if (!_seeders.IsEmpty) return true;
158+
159+
logger.LogWarning("No seeders were added prior to execution.");
160+
return false;
161+
162+
}
163+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// ---------------------------------------------------------------------------------------------------------------------
2+
// Imports
3+
// ---------------------------------------------------------------------------------------------------------------------
4+
using Microsoft.Extensions.Logging;
5+
6+
namespace CodeOfChaos.Types;
7+
8+
// ---------------------------------------------------------------------------------------------------------------------
9+
// Code
10+
// ---------------------------------------------------------------------------------------------------------------------
11+
public abstract class Seeder : ISeeder {
12+
public async Task StartAsync(ILogger logger, CancellationToken ct = default) {
13+
if (!await ShouldSeedAsync(ct)) {
14+
logger.LogInformation("Skipping seeding");
15+
return;
16+
}
17+
ct.ThrowIfCancellationRequested();
18+
await SeedAsync(ct);
19+
}
20+
21+
public virtual Task<bool> ShouldSeedAsync(CancellationToken ct = default) => Task.FromResult(true);
22+
public abstract Task SeedAsync(CancellationToken ct = default);
23+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// ---------------------------------------------------------------------------------------------------------------------
2+
// Imports
3+
// ---------------------------------------------------------------------------------------------------------------------
4+
using Microsoft.Extensions.DependencyInjection;
5+
using System.Collections;
6+
using System.Collections.Concurrent;
7+
8+
namespace CodeOfChaos.Types;
9+
// ---------------------------------------------------------------------------------------------------------------------
10+
// Code
11+
// ---------------------------------------------------------------------------------------------------------------------
12+
public readonly struct SeederGroup(IServiceProvider serviceProvider) : IEnumerable<ISeeder> {
13+
private readonly ConcurrentQueue<ISeeder> _seeders = [];
14+
public bool IsEmpty => _seeders.IsEmpty;
15+
public int Count => _seeders.Count;
16+
17+
// -----------------------------------------------------------------------------------------------------------------
18+
// Constructors
19+
// -----------------------------------------------------------------------------------------------------------------
20+
public SeederGroup(ISeeder[] seeders, IServiceProvider serviceProvider) : this(serviceProvider) {
21+
foreach (ISeeder seeder in seeders) _seeders.Enqueue(seeder);
22+
}
23+
24+
// -----------------------------------------------------------------------------------------------------------------
25+
// Methods
26+
// -----------------------------------------------------------------------------------------------------------------
27+
public SeederGroup AddSeeder<T>(Func<IServiceProvider, T> seederFactory) where T : ISeeder
28+
=> AddSeeder(seederFactory(serviceProvider));
29+
30+
public SeederGroup AddSeeder<T>(Func<T> seederFactory) where T : ISeeder
31+
=> AddSeeder(seederFactory());
32+
33+
public SeederGroup AddSeeder<T>() where T : ISeeder
34+
=> AddSeeder(serviceProvider.GetRequiredService<T>());
35+
36+
public SeederGroup AddSeeder(Type type)
37+
=> AddSeeder(ActivatorUtilities.CreateInstance<ISeeder>(serviceProvider, type));
38+
39+
public SeederGroup AddSeeder(ISeeder seeder) {
40+
if (_seeders.Any(existing => existing.Equals(seeder))) {
41+
throw new InvalidOperationException($"Seeder instance '{seeder}' has already been added.");
42+
}
43+
44+
_seeders.Enqueue(seeder);
45+
return this;
46+
}
47+
48+
public IEnumerator<ISeeder> GetEnumerator() => _seeders.GetEnumerator();
49+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
50+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// ---------------------------------------------------------------------------------------------------------------------
2+
// Imports
3+
// ---------------------------------------------------------------------------------------------------------------------
4+
using CodeOfChaos.Types;
5+
6+
// ReSharper disable once CheckNamespace
7+
namespace Microsoft.Extensions.DependencyInjection;
8+
9+
// ---------------------------------------------------------------------------------------------------------------------
10+
// Code
11+
// ---------------------------------------------------------------------------------------------------------------------
12+
public static class ServiceCollectionExtensions {
13+
public static IServiceCollection AddOneTimeDataSeeder<TDataSeeder>(this IServiceCollection services)
14+
where TDataSeeder : class, IDataSeederService
15+
=> services.AddHostedService<TDataSeeder>();
16+
17+
public static IServiceCollection AddOneTimeDataSeeder<TDataSeeder>(this IServiceCollection services, Func<IServiceProvider, TDataSeeder> implementationFactory)
18+
where TDataSeeder : class, IDataSeederService
19+
=> services.AddHostedService(implementationFactory);
20+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"profiles": {
3+
"Test": {
4+
"commandName": "Project"
5+
}
6+
}
7+
}

0 commit comments

Comments
 (0)