From 14cdd1cf1eb0add66aad1a8d04cbc599da1ba130 Mon Sep 17 00:00:00 2001 From: Sandro Ciervo Date: Thu, 3 Oct 2024 16:56:35 +0200 Subject: [PATCH 01/16] Remove SimpleInjector dependency --- .../Neolution.Extensions.DataSeeding.csproj | 3 +- Neolution.Extensions.DataSeeding/Seeder.cs | 8 +-- Neolution.Extensions.DataSeeding/Seeding.cs | 60 ++----------------- .../ServiceCollectionExtensions.cs | 18 +++++- 4 files changed, 24 insertions(+), 65 deletions(-) diff --git a/Neolution.Extensions.DataSeeding/Neolution.Extensions.DataSeeding.csproj b/Neolution.Extensions.DataSeeding/Neolution.Extensions.DataSeeding.csproj index 76fe31d..e7e2f2a 100644 --- a/Neolution.Extensions.DataSeeding/Neolution.Extensions.DataSeeding.csproj +++ b/Neolution.Extensions.DataSeeding/Neolution.Extensions.DataSeeding.csproj @@ -6,11 +6,12 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Neolution.Extensions.DataSeeding/Seeder.cs b/Neolution.Extensions.DataSeeding/Seeder.cs index 2aaae1c..cd15431 100644 --- a/Neolution.Extensions.DataSeeding/Seeder.cs +++ b/Neolution.Extensions.DataSeeding/Seeder.cs @@ -9,7 +9,7 @@ /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "Resolved as a singleton by DI container")] - internal sealed class Seeder : ISeeder, IAsyncDisposable + internal sealed class Seeder : ISeeder { /// /// The logger. @@ -74,12 +74,6 @@ public async Task SeedAsync() await seed.SeedAsync().ConfigureAwait(false); } - /// - public async ValueTask DisposeAsync() - { - await Seeding.Instance.DisposeAsync().ConfigureAwait(false); - } - /// /// Logs the wrap tree in a pretty format. /// diff --git a/Neolution.Extensions.DataSeeding/Seeding.cs b/Neolution.Extensions.DataSeeding/Seeding.cs index 860a552..7a5fc9b 100644 --- a/Neolution.Extensions.DataSeeding/Seeding.cs +++ b/Neolution.Extensions.DataSeeding/Seeding.cs @@ -4,49 +4,31 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; - using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Neolution.Extensions.DataSeeding.Abstractions; using Neolution.Extensions.DataSeeding.Internal; - using SimpleInjector; - using SimpleInjector.Lifestyles; /// /// The Seeding singleton. /// - internal sealed class Seeding : IAsyncDisposable + internal sealed class Seeding { /// /// The lazy singleton instantiation. /// private static readonly Lazy Lazy = new(() => new Seeding()); - /// - /// The services of the application that is using seeding. - /// - private IServiceCollection? services; - /// /// The assembly that contains the seeds. /// private Assembly? seedsAssembly; - /// - /// The container. - /// - private Container? container; - /// /// The seeds /// private IReadOnlyList seeds = Enumerable.Empty().ToList(); - /// - /// The scope - /// - private Scope? scope; - /// /// Prevents a default instance of the class from being created. /// @@ -64,28 +46,12 @@ private Seeding() /// internal IReadOnlyList Seeds { get; private set; } = Enumerable.Empty().ToList(); - /// - public async ValueTask DisposeAsync() - { - if (this.scope is not null) - { - await this.scope.DisposeAsync().ConfigureAwait(false); - } - - if (this.container is not null) - { - await this.container.DisposeAsync().ConfigureAwait(false); - } - } - /// /// Configures the services with the internal dependency injection container and scans the specified assembly for data seeds. /// - /// The services. /// The assembly containing the implementations. - internal void Configure(IServiceCollection services, Assembly assembly) + public void Configure(Assembly assembly) { - this.services = services; this.seedsAssembly = assembly; } @@ -95,23 +61,11 @@ internal void Configure(IServiceCollection services, Assembly assembly) /// The service provider. internal void UseServiceProvider(IServiceProvider serviceProvider) { - if (this.services is null) - { - throw new InvalidOperationException("The service collection of the application is null. Did you call the Configure() method before calling this?"); - } - if (this.seedsAssembly is null) { throw new InvalidOperationException("Cannot find the assembly containing the seeds. Did you call the Configure() method before calling this?"); } - // Create an isolated DI container just for the seeds but register the services from the application to let the seeds inject them. - this.container = new Container(); - this.services.AddSimpleInjector(this.container); - this.container.Collection.Register(new[] { this.seedsAssembly }, Lifestyle.Transient); - this.container.Collection.Register(new[] { this.seedsAssembly }, Lifestyle.Transient); - - serviceProvider.UseSimpleInjector(this.container); var logger = serviceProvider.GetRequiredService>(); if (logger.IsEnabled(LogLevel.Trace)) @@ -120,13 +74,9 @@ internal void UseServiceProvider(IServiceProvider serviceProvider) logger.LogTrace($"Assembly full name: '{this.seedsAssembly?.FullName}'"); } - // Always verify the container to avoid some runtime headaches - this.container.Verify(); - - // Open scope for container and resolve all seeds that are registered - this.scope = AsyncScopedLifestyle.BeginScope(this.container); - this.Seeds = this.container.GetAllInstances().ToList(); - this.seeds = this.container.GetAllInstances().ToList(); + // Resolve all seeds that are registered + this.Seeds = serviceProvider.GetServices().ToList(); + this.seeds = serviceProvider.GetServices().ToList(); logger.LogDebug($"{this.Seeds.Count} seeds have been found and loaded"); logger.LogDebug($"Seeding instance ready"); diff --git a/Neolution.Extensions.DataSeeding/ServiceCollectionExtensions.cs b/Neolution.Extensions.DataSeeding/ServiceCollectionExtensions.cs index 8ba4985..822adf3 100644 --- a/Neolution.Extensions.DataSeeding/ServiceCollectionExtensions.cs +++ b/Neolution.Extensions.DataSeeding/ServiceCollectionExtensions.cs @@ -16,8 +16,22 @@ public static class ServiceCollectionExtensions /// The assembly. public static void AddDataSeeding(this IServiceCollection services, Assembly assembly) { - services.AddSingleton(); - Seeding.Instance.Configure(services, assembly); + services.AddTransient(); + Seeding.Instance.Configure(assembly); + + // Register ISeed implementations + services.Scan(scan => scan + .FromAssemblies(assembly) + .AddClasses(classes => classes.AssignableTo()) + .AsImplementedInterfaces() + .WithTransientLifetime()); + + // Register Seed implementations + services.Scan(scan => scan + .FromAssemblies(assembly) + .AddClasses(classes => classes.AssignableTo()) + .AsSelf() + .WithTransientLifetime()); } } } From f4d80dd0c6868b2c3ea13db58bc935727fb03ebe Mon Sep 17 00:00:00 2001 From: Sandro Ciervo Date: Thu, 3 Oct 2024 17:26:08 +0200 Subject: [PATCH 02/16] Downgrade Scrutor --- .../packages.lock.json | 63 ++----------------- .../Neolution.Extensions.DataSeeding.csproj | 2 +- 2 files changed, 7 insertions(+), 58 deletions(-) diff --git a/Neolution.Extensions.DataSeeding.Sample/packages.lock.json b/Neolution.Extensions.DataSeeding.Sample/packages.lock.json index 0cfd0a6..f929206 100644 --- a/Neolution.Extensions.DataSeeding.Sample/packages.lock.json +++ b/Neolution.Extensions.DataSeeding.Sample/packages.lock.json @@ -207,11 +207,6 @@ "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0" } }, - "Microsoft.Extensions.Localization.Abstractions": { - "type": "Transitive", - "resolved": "6.0.16", - "contentHash": "eRCoZcG/6YqGJQrYziPOc2SmbgJq2ZtP6A9dnvc+9QBDvBHMQZLijpX68NbjGP4OlBpVId512uyuB72boOofuA==" - }, "Microsoft.Extensions.Logging": { "type": "Transitive", "resolved": "6.0.0", @@ -226,8 +221,8 @@ }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", - "resolved": "6.0.3", - "contentHash": "SUpStcdjeBbdKjPKe53hVVLkFjylX0yIXY8K+xWa47+o1d+REDyOMZjHZa+chsQI1K9qZeiHWk9jos0TFU7vGg==" + "resolved": "6.0.4", + "contentHash": "K14wYgwOfKVELrUh5eBqlC8Wvo9vvhS3ZhIvcswV2uS/ubkTRPSQsN557EZiYUSSoZNxizG+alN4wjtdyLdcyw==" }, "Microsoft.Extensions.Logging.Configuration": { "type": "Transitive", @@ -273,16 +268,6 @@ "System.Runtime.CompilerServices.Unsafe": "6.0.0" } }, - "Microsoft.NETCore.Platforms": { - "type": "Transitive", - "resolved": "1.0.1", - "contentHash": "2G6OjjJzwBfNOO8myRV/nFrbTw5iA+DEm0N+qUqhrOmaVtn4pC77h38I1jsXGw5VH55+dPfQsqHD0We9sCl9FQ==" - }, - "Microsoft.NETCore.Targets": { - "type": "Transitive", - "resolved": "1.0.1", - "contentHash": "rkn+fKobF/cbWfnnfBOQHKVKIOpxMZBvlSHkqDWgBpwGDcLRduvs3D9OLGeV6GWGvVwNlVi2CBbTjuPmtHvyNw==" - }, "NLog": { "type": "Transitive", "resolved": "4.7.14", @@ -300,33 +285,13 @@ }, "Scrutor": { "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "qZ8GKk9qpD7/ohBrxAz36W8D4smGvnkBhtAvD1c6y8jG3rH0hgt/W4vJEcAOVsZME6Rl1cemiuTZjC4cmEP02g==", + "resolved": "4.2.2", + "contentHash": "t5VIYA7WJXoJJo7s4DoHakMGwTu+MeEnZumMOhTCH7kz9xWha24G7dJNxWrHPlu0ZdZAS4jDZCxxAnyaBh7uYw==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", "Microsoft.Extensions.DependencyModel": "6.0.0" } }, - "SimpleInjector": { - "type": "Transitive", - "resolved": "5.4.0", - "contentHash": "aI00zZSTPl/NaNUvcMqnMQa0q2qMePrVqFc3LDx+fFsMb+eGZOiPDHtFaCW+EKExDqfKxpoJKvacq7JOEFK/HQ==", - "dependencies": { - "System.ComponentModel": "4.0.1" - } - }, - "SimpleInjector.Integration.ServiceCollection": { - "type": "Transitive", - "resolved": "5.5.0", - "contentHash": "+WhjLRtJ6zSSfQRDk71d+RXIkVThfEzNJPmOGCA1O6q2a/srhZxG+wGMhqAK5K2E0fn9ea1w3zB/wxyWpjrbaQ==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", - "Microsoft.Extensions.Hosting.Abstractions": "6.0.0", - "Microsoft.Extensions.Localization.Abstractions": "6.0.16", - "Microsoft.Extensions.Logging.Abstractions": "6.0.3", - "SimpleInjector": "5.4.0" - } - }, "SonarAnalyzer.CSharp": { "type": "Transitive", "resolved": "8.56.0.67649", @@ -342,14 +307,6 @@ "resolved": "4.5.1", "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" }, - "System.ComponentModel": { - "type": "Transitive", - "resolved": "4.0.1", - "contentHash": "oBZFnm7seFiVfugsIyOvQCWobNZs7FzqDV/B7tx20Ep/l3UUFCPDkdTnCNaJZTU27zjeODmy2C/cP60u3D4c9w==", - "dependencies": { - "System.Runtime": "4.1.0" - } - }, "System.Diagnostics.DiagnosticSource": { "type": "Transitive", "resolved": "6.0.0", @@ -363,15 +320,6 @@ "resolved": "4.5.4", "contentHash": "1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw==" }, - "System.Runtime": { - "type": "Transitive", - "resolved": "4.1.0", - "contentHash": "v6c/4Yaa9uWsq+JMhnOFewrYkgdNHNG2eMKuNqRn8P733rNXeRCGvV5FkkjBXn2dbVkPXOsO0xjsEeM1q2zC0g==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1", - "Microsoft.NETCore.Targets": "1.0.1" - } - }, "System.Runtime.CompilerServices.Unsafe": { "type": "Transitive", "resolved": "6.0.0", @@ -397,7 +345,8 @@ "neolution.extensions.dataseeding": { "type": "Project", "dependencies": { - "SimpleInjector.Integration.ServiceCollection": "[5.5.0, )" + "Microsoft.Extensions.Logging.Abstractions": "[6.0.4, )", + "Scrutor": "[4.2.2, )" } } } diff --git a/Neolution.Extensions.DataSeeding/Neolution.Extensions.DataSeeding.csproj b/Neolution.Extensions.DataSeeding/Neolution.Extensions.DataSeeding.csproj index e7e2f2a..ad4f13b 100644 --- a/Neolution.Extensions.DataSeeding/Neolution.Extensions.DataSeeding.csproj +++ b/Neolution.Extensions.DataSeeding/Neolution.Extensions.DataSeeding.csproj @@ -11,7 +11,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + From 9365c55c7dc8dda94dbc31560de5e76df28362a8 Mon Sep 17 00:00:00 2001 From: Sandro Ciervo Date: Thu, 17 Jul 2025 15:13:14 +0200 Subject: [PATCH 03/16] Upgrade Scrutor --- .../packages.lock.json | 45 +++++++------------ .../Neolution.Extensions.DataSeeding.csproj | 2 +- 2 files changed, 17 insertions(+), 30 deletions(-) diff --git a/Neolution.Extensions.DataSeeding.Sample/packages.lock.json b/Neolution.Extensions.DataSeeding.Sample/packages.lock.json index f929206..2187d03 100644 --- a/Neolution.Extensions.DataSeeding.Sample/packages.lock.json +++ b/Neolution.Extensions.DataSeeding.Sample/packages.lock.json @@ -159,19 +159,16 @@ }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "xlzi2IYREJH3/m6+lUrQlujzX8wDitm4QGnUu6kUXTQAWPuZY8i+ticFJbzfqaetLA6KR/rO6Ew/HuYD+bxifg==" + "resolved": "8.0.1", + "contentHash": "fGLiCRLMYd00JYpClraLjJTNKLmMJPnqxMaiRzEBIIvevlzxz33mXy39Lkd48hu1G+N21S7QpaO5ZzKsI6FRuA==" }, "Microsoft.Extensions.DependencyModel": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "TD5QHg98m3+QhgEV1YVoNMl5KtBw/4rjfxLHO0e/YV9bPUBDKntApP4xdrVtGgCeQZHVfC2EXIGsdpRNrr87Pg==", + "resolved": "8.0.2", + "contentHash": "mUBDZZRgZrSyFOsJ2qJJ9fXfqd/kXJwf3AiDoqLD9m6TjY5OO/vLNOb9fb4juC0487eq4hcGN/M2Rh/CKS7QYw==", "dependencies": { - "System.Buffers": "4.5.1", - "System.Memory": "4.5.4", - "System.Runtime.CompilerServices.Unsafe": "6.0.0", - "System.Text.Encodings.Web": "6.0.0", - "System.Text.Json": "6.0.0" + "System.Text.Encodings.Web": "8.0.0", + "System.Text.Json": "8.0.5" } }, "Microsoft.Extensions.FileProviders.Abstractions": { @@ -285,11 +282,11 @@ }, "Scrutor": { "type": "Transitive", - "resolved": "4.2.2", - "contentHash": "t5VIYA7WJXoJJo7s4DoHakMGwTu+MeEnZumMOhTCH7kz9xWha24G7dJNxWrHPlu0ZdZAS4jDZCxxAnyaBh7uYw==", + "resolved": "6.1.0", + "contentHash": "m4+0RdgnX+jeiaqteq9x5SwEtuCjWG0KTw1jBjCzn7V8mCanXKoeF8+59E0fcoRbAjdEq6YqHFCmxZ49Kvqp3g==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", - "Microsoft.Extensions.DependencyModel": "6.0.0" + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.1", + "Microsoft.Extensions.DependencyModel": "8.0.2" } }, "SonarAnalyzer.CSharp": { @@ -302,11 +299,6 @@ "resolved": "1.2.0.556", "contentHash": "zvn9Mqs/ox/83cpYPignI8hJEM2A93s2HkHs8HYMOAQW0PkampyoErAiIyKxgTLqbbad29HX/shv/6LGSjPJNQ==" }, - "System.Buffers": { - "type": "Transitive", - "resolved": "4.5.1", - "contentHash": "Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==" - }, "System.Diagnostics.DiagnosticSource": { "type": "Transitive", "resolved": "6.0.0", @@ -315,11 +307,6 @@ "System.Runtime.CompilerServices.Unsafe": "6.0.0" } }, - "System.Memory": { - "type": "Transitive", - "resolved": "4.5.4", - "contentHash": "1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw==" - }, "System.Runtime.CompilerServices.Unsafe": { "type": "Transitive", "resolved": "6.0.0", @@ -327,26 +314,26 @@ }, "System.Text.Encodings.Web": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "Vg8eB5Tawm1IFqj4TVK1czJX89rhFxJo9ELqc/Eiq0eXy13RK00eubyU6TJE6y+GQXjyV5gSfiewDUZjQgSE0w==", + "resolved": "8.0.0", + "contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==", "dependencies": { "System.Runtime.CompilerServices.Unsafe": "6.0.0" } }, "System.Text.Json": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "zaJsHfESQvJ11vbXnNlkrR46IaMULk/gHxYsJphzSF+07kTjPHv+Oc14w6QEOfo3Q4hqLJgStUaYB9DBl0TmWg==", + "resolved": "8.0.5", + "contentHash": "0f1B50Ss7rqxXiaBJyzUu9bWFOO2/zSlifZ/UNMdiIpDYe4cY4LQQicP4nirK1OS31I43rn062UIJ1Q9bpmHpg==", "dependencies": { "System.Runtime.CompilerServices.Unsafe": "6.0.0", - "System.Text.Encodings.Web": "6.0.0" + "System.Text.Encodings.Web": "8.0.0" } }, "neolution.extensions.dataseeding": { "type": "Project", "dependencies": { "Microsoft.Extensions.Logging.Abstractions": "[6.0.4, )", - "Scrutor": "[4.2.2, )" + "Scrutor": "[6.1.0, )" } } } diff --git a/Neolution.Extensions.DataSeeding/Neolution.Extensions.DataSeeding.csproj b/Neolution.Extensions.DataSeeding/Neolution.Extensions.DataSeeding.csproj index ad4f13b..7c36cc8 100644 --- a/Neolution.Extensions.DataSeeding/Neolution.Extensions.DataSeeding.csproj +++ b/Neolution.Extensions.DataSeeding/Neolution.Extensions.DataSeeding.csproj @@ -11,7 +11,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + From 4f65ae337946c693925ac61259c7485483ca141f Mon Sep 17 00:00:00 2001 From: Sandro Ciervo Date: Thu, 17 Jul 2025 15:52:32 +0200 Subject: [PATCH 04/16] Bump several NuGet package dependencies to their latest versions. --- ...ution.Extensions.DataSeeding.Sample.csproj | 7 +- .../packages.lock.json | 130 +++++++++--------- ...on.Extensions.DataSeeding.UnitTests.csproj | 13 +- .../Neolution.Extensions.DataSeeding.csproj | 2 +- Neolution.Extensions.DataSeeding/Seeder.cs | 7 +- Neolution.Extensions.DataSeeding/Seeding.cs | 6 +- 6 files changed, 80 insertions(+), 85 deletions(-) diff --git a/Neolution.Extensions.DataSeeding.Sample/Neolution.Extensions.DataSeeding.Sample.csproj b/Neolution.Extensions.DataSeeding.Sample/Neolution.Extensions.DataSeeding.Sample.csproj index f44289b..e277add 100644 --- a/Neolution.Extensions.DataSeeding.Sample/Neolution.Extensions.DataSeeding.Sample.csproj +++ b/Neolution.Extensions.DataSeeding.Sample/Neolution.Extensions.DataSeeding.Sample.csproj @@ -1,4 +1,4 @@ - + Exe net6.0 @@ -8,9 +8,10 @@ - + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Neolution.Extensions.DataSeeding.Sample/packages.lock.json b/Neolution.Extensions.DataSeeding.Sample/packages.lock.json index 2187d03..33420ad 100644 --- a/Neolution.Extensions.DataSeeding.Sample/packages.lock.json +++ b/Neolution.Extensions.DataSeeding.Sample/packages.lock.json @@ -13,24 +13,37 @@ } }, "Microsoft.Extensions.Configuration.UserSecrets": { + "type": "Direct", + "requested": "[6.0.2, )", + "resolved": "6.0.2", + "contentHash": "q3zRs1RbaNRHvDaooOOdJs4KMbaVg6MJUgPD60p1DuOMDjob5LbbBYRZwdaas/TrkCUZ7vYSBn3EXrrDWY1HSA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "6.0.1", + "Microsoft.Extensions.Configuration.Json": "6.0.1", + "Microsoft.Extensions.FileProviders.Abstractions": "6.0.1", + "Microsoft.Extensions.FileProviders.Physical": "6.0.1" + } + }, + "Microsoft.Extensions.Logging": { "type": "Direct", "requested": "[6.0.1, )", "resolved": "6.0.1", - "contentHash": "Fy8yr4V6obi7ZxvKYI1i85jqtwMq8tqyxQVZpRSkgeA8enqy/KvBIMdcuNdznlxQMZa72mvbHqb7vbg4Pyx95w==", + "contentHash": "k6tbYaHrqY9kq7p5FfpPbddY1OImPCpXQ/PGcED6N9s5ULRp8n1PdmMzsIwIzCnhIS5bs06G/lO9LfNVpUj8jg==", "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "6.0.0", - "Microsoft.Extensions.Configuration.Json": "6.0.0", - "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0", - "Microsoft.Extensions.FileProviders.Physical": "6.0.0" + "Microsoft.Extensions.DependencyInjection": "6.0.2", + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "Microsoft.Extensions.Logging.Abstractions": "6.0.4", + "Microsoft.Extensions.Options": "6.0.1", + "System.Diagnostics.DiagnosticSource": "6.0.2" } }, "Neolution.CodeAnalysis": { "type": "Direct", - "requested": "[3.1.2, )", - "resolved": "3.1.2", - "contentHash": "pdKfYvkYwLD8/CIQdJrVFHXcOTX+7uHHxcJSqtzGx05BGgN3rSEC+Fd7SRQtr5x3Q0gfOYoMhemoz+1l6NeUpA==", + "requested": "[3.2.1, )", + "resolved": "3.2.1", + "contentHash": "AQDkBJ9e6TrnhpwTtf4KrwlAPBLy77I5XkNey586nN2IFfjk1LVwBwvOcjORD1dSNtEhOf69JQjdmiMf54C90w==", "dependencies": { - "SonarAnalyzer.CSharp": "8.56.0.67649", + "SonarAnalyzer.CSharp": "9.20.0.85982", "StyleCop.Analyzers.Unstable": "1.2.0.556" } }, @@ -83,19 +96,19 @@ }, "Microsoft.Extensions.Configuration": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "tq2wXyh3fL17EMF2bXgRhU7JrbO3on93MRKYxzz4JzzvuGSA1l0W3GI9/tl8EO89TH+KWEymP7bcFway6z9fXg==", + "resolved": "6.0.2", + "contentHash": "2dpWx3Lxqq1lzLa9pY4lTOl6xg7VL45z0oe3r1xfXM0nQzR9XpuuMdk54B1LrC/AsyKbOukry+pdoQ7M7e/ezg==", "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "6.0.0", - "Microsoft.Extensions.Primitives": "6.0.0" + "Microsoft.Extensions.Configuration.Abstractions": "6.0.1", + "Microsoft.Extensions.Primitives": "6.0.1" } }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "qWzV9o+ZRWq+pGm+1dF+R7qTgTYoXvbyowRoBxQJGfqTpqDun2eteerjRQhq5PQ/14S+lqto3Ft4gYaRyl4rdQ==", + "resolved": "6.0.1", + "contentHash": "+FhuIM7bE3CyzhhIju6K1pmcDAp4ez2PKwx8jnV4dEI/LXXBGdQbDijlaQWqMa1oC38DX390bSshQVaKXioiXA==", "dependencies": { - "Microsoft.Extensions.Primitives": "6.0.0" + "Microsoft.Extensions.Primitives": "6.0.1" } }, "Microsoft.Extensions.Configuration.Binder": { @@ -126,35 +139,34 @@ }, "Microsoft.Extensions.Configuration.FileExtensions": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "V4Dth2cYMZpw3HhGw9XUDIijpI6gN+22LDt0AhufIgOppCUfpWX4483OmN+dFXRJkJLc8Tv0Q8QK+1ingT2+KQ==", + "resolved": "6.0.1", + "contentHash": "2jaVZJqN6BvEV9DR/gqOnZUY1jjA/NaUcyJVl7Mp3Uvb9N0YE2uSJ6bdmiKxocsiGZxpfjqBe0SRISCCKhVecw==", "dependencies": { - "Microsoft.Extensions.Configuration": "6.0.0", - "Microsoft.Extensions.Configuration.Abstractions": "6.0.0", - "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0", - "Microsoft.Extensions.FileProviders.Physical": "6.0.0", - "Microsoft.Extensions.Primitives": "6.0.0" + "Microsoft.Extensions.Configuration": "6.0.2", + "Microsoft.Extensions.Configuration.Abstractions": "6.0.1", + "Microsoft.Extensions.FileProviders.Abstractions": "6.0.1", + "Microsoft.Extensions.FileProviders.Physical": "6.0.1", + "Microsoft.Extensions.Primitives": "6.0.1" } }, "Microsoft.Extensions.Configuration.Json": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "GJGery6QytCzS/BxJ96klgG9in3uH26KcUBbiVG/coNDXCRq6LGVVlUT4vXq34KPuM+R2av+LeYdX9h4IZOCUg==", + "resolved": "6.0.1", + "contentHash": "1jgLdq/UXJ71TE0toUF5VPn7yYaeyiN9sAdX2EZ6Z5DbUiWUKcMRAS5biExfu/ChYMvlGH/7ZlJijPY0zB1Vhg==", "dependencies": { - "Microsoft.Extensions.Configuration": "6.0.0", - "Microsoft.Extensions.Configuration.Abstractions": "6.0.0", - "Microsoft.Extensions.Configuration.FileExtensions": "6.0.0", - "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0", - "System.Text.Json": "6.0.0" + "Microsoft.Extensions.Configuration": "6.0.2", + "Microsoft.Extensions.Configuration.Abstractions": "6.0.1", + "Microsoft.Extensions.Configuration.FileExtensions": "6.0.1", + "Microsoft.Extensions.FileProviders.Abstractions": "6.0.1", + "System.Text.Json": "6.0.11" } }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "k6PWQMuoBDGGHOQTtyois2u4AwyVcIwL2LaSLlTZQm2CYcJ1pxbt6jfAnpWmzENA/wfrYRI/X9DTLoUkE4AsLw==", + "resolved": "6.0.2", + "contentHash": "gWUfUZ2ZDvwiVCxsOMComAhG43xstNWWVjV2takUZYRuDSJjO9Q5/b3tfOSkl5mcVwZAL3RZviRj5ZilxHghlw==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", - "System.Runtime.CompilerServices.Unsafe": "6.0.0" + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { @@ -173,20 +185,20 @@ }, "Microsoft.Extensions.FileProviders.Abstractions": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "0pd4/fho0gC12rQswaGQxbU34jOS1TPS8lZPpkFCH68ppQjHNHYle9iRuHeev1LhrJ94YPvzcRd8UmIuFk23Qw==", + "resolved": "6.0.1", + "contentHash": "Kr8S/Uunxsjyhh5D91avbME3pITDrVYp/NACiymemtwhnvxpe82Jmr99cnPqiVridg2nIkcNNRVKzl/iP5/00g==", "dependencies": { - "Microsoft.Extensions.Primitives": "6.0.0" + "Microsoft.Extensions.Primitives": "6.0.1" } }, "Microsoft.Extensions.FileProviders.Physical": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "QvkL7l0nM8udt3gfyu0Vw8bbCXblxaKOl7c2oBfgGy4LCURRaL9XWZX1FWJrQc43oMokVneVxH38iz+bY1sbhg==", + "resolved": "6.0.1", + "contentHash": "k9h04ZJiQ7mr4hsLNCUc/CL4AwOAHKq0uam4JBfeZ33XSU5MHxn7l7Vpf+T5sd5HXI7us1MGrkB5awqfg5xyHw==", "dependencies": { - "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "6.0.1", "Microsoft.Extensions.FileSystemGlobbing": "6.0.0", - "Microsoft.Extensions.Primitives": "6.0.0" + "Microsoft.Extensions.Primitives": "6.0.1" } }, "Microsoft.Extensions.FileSystemGlobbing": { @@ -204,18 +216,6 @@ "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0" } }, - "Microsoft.Extensions.Logging": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "eIbyj40QDg1NDz0HBW0S5f3wrLVnKWnDJ/JtZ+yJDFnDj90VoPuoPmFkeaXrtu+0cKm5GRAwoDf+dBWXK0TUdg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection": "6.0.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", - "Microsoft.Extensions.Logging.Abstractions": "6.0.0", - "Microsoft.Extensions.Options": "6.0.0", - "System.Diagnostics.DiagnosticSource": "6.0.0" - } - }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", "resolved": "6.0.4", @@ -238,11 +238,11 @@ }, "Microsoft.Extensions.Options": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "dzXN0+V1AyjOe2xcJ86Qbo233KHuLEY0njf/P2Kw8SfJU+d45HNS2ctJdnEnrWbM9Ye2eFgaC5Mj9otRMU6IsQ==", + "resolved": "6.0.1", + "contentHash": "v5rh5jRcLBOKOaLVyYCm4TY/RoJlxWsW7N2TAPkmlHe55/0cB0Syp979x4He1+MIXsaTvJl1WOc7b1D1PSsO3A==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", - "Microsoft.Extensions.Primitives": "6.0.0" + "Microsoft.Extensions.Primitives": "6.0.1" } }, "Microsoft.Extensions.Options.ConfigurationExtensions": { @@ -259,11 +259,8 @@ }, "Microsoft.Extensions.Primitives": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "9+PnzmQFfEFNR9J2aDTfJGGupShHjOuGw4VUv+JB044biSHrnmCIMD+mJHmb2H7YryrfBEXDurxQ47gJZdCKNQ==", - "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "6.0.0" - } + "resolved": "6.0.1", + "contentHash": "zyJfttJduuQbGEsd/TgerUEdgzHgUn/6oFgeaE04DKUMs7bbzckV7Ci1QZxf/VyQAlG41gR/GjecEScuYoHCUg==" }, "NLog": { "type": "Transitive", @@ -291,8 +288,8 @@ }, "SonarAnalyzer.CSharp": { "type": "Transitive", - "resolved": "8.56.0.67649", - "contentHash": "u5klyn4PBAOe38/CoPbxGjuMqJQ5K0M7SA17H2DHgHyQ+neSU+abneRGjSQSVD76kbYXzG7E1/KQwTuj+nnMZw==" + "resolved": "9.20.0.85982", + "contentHash": "c0IYtFg4mYusTafTy0Bs5wev45vRKNShkuoWyQyPMbC6imEFHL7tY/7xbbUJd6JNLYx5vP8wyi2LgiBsMHqb2Q==" }, "StyleCop.Analyzers.Unstable": { "type": "Transitive", @@ -301,11 +298,8 @@ }, "System.Diagnostics.DiagnosticSource": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "frQDfv0rl209cKm1lnwTgFPzNigy2EKk1BS3uAvHvlBVKe5cymGyHO+Sj+NLv5VF/AhHsqPIUUwya5oV4CHMUw==", - "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "6.0.0" - } + "resolved": "6.0.2", + "contentHash": "6tQaIexFycaotdGn23lf3XJ/eI1GOjQKIvQDRFN9N4pwoNsKnHuXccQ3lnQO6GX8KAb1ic+6ZofJmPdbUVwZag==" }, "System.Runtime.CompilerServices.Unsafe": { "type": "Transitive", diff --git a/Neolution.Extensions.DataSeeding.UnitTests/Neolution.Extensions.DataSeeding.UnitTests.csproj b/Neolution.Extensions.DataSeeding.UnitTests/Neolution.Extensions.DataSeeding.UnitTests.csproj index 5866df9..46ee334 100644 --- a/Neolution.Extensions.DataSeeding.UnitTests/Neolution.Extensions.DataSeeding.UnitTests.csproj +++ b/Neolution.Extensions.DataSeeding.UnitTests/Neolution.Extensions.DataSeeding.UnitTests.csproj @@ -9,20 +9,19 @@ - - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Neolution.Extensions.DataSeeding/Neolution.Extensions.DataSeeding.csproj b/Neolution.Extensions.DataSeeding/Neolution.Extensions.DataSeeding.csproj index 7c36cc8..de098f9 100644 --- a/Neolution.Extensions.DataSeeding/Neolution.Extensions.DataSeeding.csproj +++ b/Neolution.Extensions.DataSeeding/Neolution.Extensions.DataSeeding.csproj @@ -7,7 +7,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Neolution.Extensions.DataSeeding/Seeder.cs b/Neolution.Extensions.DataSeeding/Seeder.cs index cd15431..8999e4c 100644 --- a/Neolution.Extensions.DataSeeding/Seeder.cs +++ b/Neolution.Extensions.DataSeeding/Seeder.cs @@ -2,13 +2,14 @@ { using System; using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Neolution.Extensions.DataSeeding.Abstractions; using Neolution.Extensions.DataSeeding.Internal; /// - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "Resolved as a singleton by DI container")] + [SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "Resolved as a singleton by DI container")] internal sealed class Seeder : ISeeder { /// @@ -53,7 +54,7 @@ public async Task SeedAsync() for (var index = 0; index < sortedSeeds.Count; index++) { var seed = sortedSeeds[index]; - this.logger.LogTrace($"{index + 1}.\t{seed.GetType().Name}"); + this.logger.LogTrace("{Index}.\t{Name}", index + 1, seed.GetType().Name); } } @@ -91,7 +92,7 @@ private void LogWrapTree(Wrap wrap, bool last, string indent = "") seedTypeName = seedTypeName[..^suffix.Length]; } - this.logger.LogDebug($"{indent}+- {seedTypeName}"); + this.logger.LogDebug("{Indent}+- {SeedTypeName}", indent, seedTypeName); indent += last ? " " : "| "; for (var i = 0; i < wrap.Wrapped.Count; i++) diff --git a/Neolution.Extensions.DataSeeding/Seeding.cs b/Neolution.Extensions.DataSeeding/Seeding.cs index 7a5fc9b..7910206 100644 --- a/Neolution.Extensions.DataSeeding/Seeding.cs +++ b/Neolution.Extensions.DataSeeding/Seeding.cs @@ -71,15 +71,15 @@ internal void UseServiceProvider(IServiceProvider serviceProvider) if (logger.IsEnabled(LogLevel.Trace)) { logger.LogTrace("Scan configured assembly for seeds"); - logger.LogTrace($"Assembly full name: '{this.seedsAssembly?.FullName}'"); + logger.LogTrace("Assembly full name: '{SeedsAssemblyFullName}'", this.seedsAssembly.FullName); } // Resolve all seeds that are registered this.Seeds = serviceProvider.GetServices().ToList(); this.seeds = serviceProvider.GetServices().ToList(); - logger.LogDebug($"{this.Seeds.Count} seeds have been found and loaded"); - logger.LogDebug($"Seeding instance ready"); + logger.LogDebug("{SeedsCount} seeds have been found and loaded", this.Seeds.Count); + logger.LogDebug("Seeding instance ready"); } /// From 829c3f50cb33b806832eab39cac158af1830611c Mon Sep 17 00:00:00 2001 From: Sandro Ciervo Date: Thu, 17 Jul 2025 15:55:53 +0200 Subject: [PATCH 05/16] cleanup --- Neolution.Extensions.DataSeeding/Seeder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Neolution.Extensions.DataSeeding/Seeder.cs b/Neolution.Extensions.DataSeeding/Seeder.cs index 8999e4c..4bb8d1f 100644 --- a/Neolution.Extensions.DataSeeding/Seeder.cs +++ b/Neolution.Extensions.DataSeeding/Seeder.cs @@ -54,7 +54,7 @@ public async Task SeedAsync() for (var index = 0; index < sortedSeeds.Count; index++) { var seed = sortedSeeds[index]; - this.logger.LogTrace("{Index}.\t{Name}", index + 1, seed.GetType().Name); + this.logger.LogTrace("{Index}.\t{SeedTypeName}", index + 1, seed.GetType().Name); } } From 48033822028564b33c7897c49dd4ab6361419f55 Mon Sep 17 00:00:00 2001 From: Sandro Ciervo Date: Thu, 17 Jul 2025 16:25:57 +0200 Subject: [PATCH 06/16] Update the data seeding logic to correctly handle abstract seed types And added unit tests --- .../AbstractSeedTests.cs | 85 +++++++++++++++++++ .../BasicTests.cs | 3 +- .../MultiTenantSeeds/AbstractSeedExample.cs | 38 +++++++++ .../ServiceCollectionExtensions.cs | 2 +- 4 files changed, 125 insertions(+), 3 deletions(-) create mode 100644 Neolution.Extensions.DataSeeding.UnitTests/AbstractSeedTests.cs create mode 100644 Neolution.Extensions.DataSeeding.UnitTests/Fakes/MultiTenantSeeds/AbstractSeedExample.cs diff --git a/Neolution.Extensions.DataSeeding.UnitTests/AbstractSeedTests.cs b/Neolution.Extensions.DataSeeding.UnitTests/AbstractSeedTests.cs new file mode 100644 index 0000000..2d3ad6f --- /dev/null +++ b/Neolution.Extensions.DataSeeding.UnitTests/AbstractSeedTests.cs @@ -0,0 +1,85 @@ +namespace Neolution.Extensions.DataSeeding.UnitTests +{ + using System.Linq; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Logging; + using Neolution.Extensions.DataSeeding.Abstractions; + using Neolution.Extensions.DataSeeding.UnitTests.Fakes; + using Neolution.Extensions.DataSeeding.UnitTests.Fakes.MultiTenantSeeds; + using Shouldly; + using Xunit; + using Xunit.Abstractions; + + /// + /// Tests for seeds that inherit from the abstract Seed class. + /// + public class AbstractSeedTests + { + /// + /// The test output helper + /// + private readonly ITestOutputHelper testOutputHelper; + + /// + /// Initializes a new instance of the class. + /// + /// The test output helper. + public AbstractSeedTests(ITestOutputHelper testOutputHelper) + { + this.testOutputHelper = testOutputHelper; + } + + /// + /// Tests that seeds inheriting from the abstract Seed class can be registered and resolved. + /// + [Fact] + public void AbstractSeedCanBeRegisteredAndResolved() + { + // Arrange + var services = this.CreateServiceCollection(); + services.AddDataSeeding(typeof(AbstractSeedTests).Assembly); + services.AddTransient(); + var serviceProvider = services.BuildServiceProvider(); + + // Act & Assert - This should not throw + var abstractSeeds = serviceProvider.GetServices(); + abstractSeeds.ShouldNotBeNull(); + abstractSeeds.ShouldNotBeEmpty(); + + // Verify we can find our specific abstract seed + var abstractSeedExample = abstractSeeds.OfType().FirstOrDefault(); + abstractSeedExample.ShouldNotBeNull(); + } + + /// + /// Tests that the seeder can execute seeds that inherit from the abstract Seed class. + /// + [Fact] + public void SeederCanExecuteAbstractSeeds() + { + // Arrange + var services = this.CreateServiceCollection(); + services.AddDataSeeding(typeof(AbstractSeedTests).Assembly); + services.AddTransient(); + var serviceProvider = services.BuildServiceProvider(); + var dataInitializer = serviceProvider.GetRequiredService(); + + // Act + var result = dataInitializer.Run(); + + // Assert + result.ShouldBeTrue(); + } + + /// + /// Creates the service collection. + /// + /// The . + private IServiceCollection CreateServiceCollection() + { + var services = new ServiceCollection(); + services.AddLogging(builder => builder.AddXUnit(this.testOutputHelper)); + return services; + } + } +} diff --git a/Neolution.Extensions.DataSeeding.UnitTests/BasicTests.cs b/Neolution.Extensions.DataSeeding.UnitTests/BasicTests.cs index df97152..22286c9 100644 --- a/Neolution.Extensions.DataSeeding.UnitTests/BasicTests.cs +++ b/Neolution.Extensions.DataSeeding.UnitTests/BasicTests.cs @@ -3,7 +3,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Neolution.Extensions.DataSeeding.UnitTests.Fakes; - using Neolution.Extensions.DataSeeding.UnitTests.Fakes.MultiTenantSeeds; using Shouldly; using Xunit; using Xunit.Abstractions; @@ -35,7 +34,7 @@ public void SeedsCanBeSeededWithLoggingTest() { // Assign var services = this.CreateServiceCollection(); - services.AddDataSeeding(typeof(TenantsSeed).Assembly); + services.AddDataSeeding(typeof(BasicTests).Assembly); services.AddTransient(); var serviceProvider = services.BuildServiceProvider(); var dataInitializer = serviceProvider.GetRequiredService(); diff --git a/Neolution.Extensions.DataSeeding.UnitTests/Fakes/MultiTenantSeeds/AbstractSeedExample.cs b/Neolution.Extensions.DataSeeding.UnitTests/Fakes/MultiTenantSeeds/AbstractSeedExample.cs new file mode 100644 index 0000000..1fbdc17 --- /dev/null +++ b/Neolution.Extensions.DataSeeding.UnitTests/Fakes/MultiTenantSeeds/AbstractSeedExample.cs @@ -0,0 +1,38 @@ +namespace Neolution.Extensions.DataSeeding.UnitTests.Fakes.MultiTenantSeeds +{ + using System.Threading.Tasks; + using Microsoft.Extensions.Logging; + using Neolution.Extensions.DataSeeding.Abstractions; + + /// + /// Example seed that inherits from the abstract Seed class instead of implementing ISeed directly. + /// This tests the scenario where users prefer to use the abstract Seed base class. + /// + public class AbstractSeedExample : Seed + { + /// + /// The logger + /// + private readonly ILogger logger; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + public AbstractSeedExample(ILogger logger) + { + this.logger = logger; + } + + /// + public override async Task SeedAsync() + { + this.logger.LogInformation("AbstractSeedExample.SeedAsync() called"); + + // Example of using the protected SeedAsync() helper method + await SeedAsync().ConfigureAwait(false); + + this.logger.LogInformation("AbstractSeedExample.SeedAsync() completed"); + } + } +} diff --git a/Neolution.Extensions.DataSeeding/ServiceCollectionExtensions.cs b/Neolution.Extensions.DataSeeding/ServiceCollectionExtensions.cs index 822adf3..96db566 100644 --- a/Neolution.Extensions.DataSeeding/ServiceCollectionExtensions.cs +++ b/Neolution.Extensions.DataSeeding/ServiceCollectionExtensions.cs @@ -30,7 +30,7 @@ public static void AddDataSeeding(this IServiceCollection services, Assembly ass services.Scan(scan => scan .FromAssemblies(assembly) .AddClasses(classes => classes.AssignableTo()) - .AsSelf() + .As() .WithTransientLifetime()); } } From 6f72bb6ce0b0af2e4c11d07534ffe922cf25151a Mon Sep 17 00:00:00 2001 From: Sandro Ciervo Date: Thu, 17 Jul 2025 16:58:40 +0200 Subject: [PATCH 07/16] cleanup --- Neolution.Extensions.DataSeeding.UnitTests/BasicTests.cs | 2 +- Neolution.Extensions.DataSeeding/Seeding.cs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Neolution.Extensions.DataSeeding.UnitTests/BasicTests.cs b/Neolution.Extensions.DataSeeding.UnitTests/BasicTests.cs index 22286c9..eb4d364 100644 --- a/Neolution.Extensions.DataSeeding.UnitTests/BasicTests.cs +++ b/Neolution.Extensions.DataSeeding.UnitTests/BasicTests.cs @@ -53,7 +53,7 @@ public void SeedsCanBeSeededWithLoggingTest() private IServiceCollection CreateServiceCollection() { var services = new ServiceCollection(); - services.AddLogging(builder => builder.AddXUnit(this.testOutputHelper)); + services.AddLogging(builder => builder.AddXUnit(this.testOutputHelper).SetMinimumLevel(LogLevel.Debug)); return services; } } diff --git a/Neolution.Extensions.DataSeeding/Seeding.cs b/Neolution.Extensions.DataSeeding/Seeding.cs index 7910206..7c39ebe 100644 --- a/Neolution.Extensions.DataSeeding/Seeding.cs +++ b/Neolution.Extensions.DataSeeding/Seeding.cs @@ -78,7 +78,9 @@ internal void UseServiceProvider(IServiceProvider serviceProvider) this.Seeds = serviceProvider.GetServices().ToList(); this.seeds = serviceProvider.GetServices().ToList(); - logger.LogDebug("{SeedsCount} seeds have been found and loaded", this.Seeds.Count); + logger.LogDebug("{SeedsCount} seeds have been found and loaded", this.Seeds.Count + this.seeds.Count); + logger.LogTrace("{SeedCount} ISeed implementations found", this.Seeds.Count); + logger.LogTrace("{SeedCount} Seed implementations found", this.seeds.Count); logger.LogDebug("Seeding instance ready"); } From 58fbd3d151c1815dd2b1110cf62ed622f3acf77d Mon Sep 17 00:00:00 2001 From: Sandro Ciervo Date: Thu, 17 Jul 2025 17:19:54 +0200 Subject: [PATCH 08/16] add another test --- .../AbstractSeedTests.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Neolution.Extensions.DataSeeding.UnitTests/AbstractSeedTests.cs b/Neolution.Extensions.DataSeeding.UnitTests/AbstractSeedTests.cs index 2d3ad6f..05c2d20 100644 --- a/Neolution.Extensions.DataSeeding.UnitTests/AbstractSeedTests.cs +++ b/Neolution.Extensions.DataSeeding.UnitTests/AbstractSeedTests.cs @@ -71,6 +71,23 @@ public void SeederCanExecuteAbstractSeeds() result.ShouldBeTrue(); } + /// + /// Tests that abstract seeds can be manually executed via . + /// + [Fact] + public void AbstractSeedsCanBeManuallyExecuted() + { + // Arrange + var services = this.CreateServiceCollection(); + services.AddDataSeeding(typeof(AbstractSeedTests).Assembly); + var serviceProvider = services.BuildServiceProvider(); + var seeder = serviceProvider.GetRequiredService(); + + // Act & Assert - This should not throw + var seedTask = seeder.SeedAsync(); + seedTask.ShouldNotBeNull(); + } + /// /// Creates the service collection. /// From 6459cd179a7c353b5ced755a8fa548621e088341 Mon Sep 17 00:00:00 2001 From: Sandro Ciervo Date: Thu, 24 Jul 2025 14:15:33 +0200 Subject: [PATCH 09/16] Enables scoped service injection for seeds Ensures that seeds can properly resolve scoped dependencies by creating a scope and resolving the seeds within that scope. --- .../AbstractSeedTests.cs | 6 ++ .../BasicTests.cs | 6 ++ .../Fakes/FakeServices.cs | 86 +++++++++++++++++++ .../ApplicationSettingsSeed.cs | 33 ++++++- Neolution.Extensions.DataSeeding/Seeder.cs | 28 +++++- 5 files changed, 155 insertions(+), 4 deletions(-) create mode 100644 Neolution.Extensions.DataSeeding.UnitTests/Fakes/FakeServices.cs diff --git a/Neolution.Extensions.DataSeeding.UnitTests/AbstractSeedTests.cs b/Neolution.Extensions.DataSeeding.UnitTests/AbstractSeedTests.cs index 05c2d20..df4a8be 100644 --- a/Neolution.Extensions.DataSeeding.UnitTests/AbstractSeedTests.cs +++ b/Neolution.Extensions.DataSeeding.UnitTests/AbstractSeedTests.cs @@ -96,6 +96,12 @@ private IServiceCollection CreateServiceCollection() { var services = new ServiceCollection(); services.AddLogging(builder => builder.AddXUnit(this.testOutputHelper)); + + // Register fake services with different lifetimes to test scoped service injection + services.AddSingleton(); + services.AddScoped(); + services.AddTransient(); + return services; } } diff --git a/Neolution.Extensions.DataSeeding.UnitTests/BasicTests.cs b/Neolution.Extensions.DataSeeding.UnitTests/BasicTests.cs index eb4d364..b1c22fc 100644 --- a/Neolution.Extensions.DataSeeding.UnitTests/BasicTests.cs +++ b/Neolution.Extensions.DataSeeding.UnitTests/BasicTests.cs @@ -54,6 +54,12 @@ private IServiceCollection CreateServiceCollection() { var services = new ServiceCollection(); services.AddLogging(builder => builder.AddXUnit(this.testOutputHelper).SetMinimumLevel(LogLevel.Debug)); + + // Register fake services with different lifetimes to test scoped service injection + services.AddSingleton(); + services.AddScoped(); + services.AddTransient(); + return services; } } diff --git a/Neolution.Extensions.DataSeeding.UnitTests/Fakes/FakeServices.cs b/Neolution.Extensions.DataSeeding.UnitTests/Fakes/FakeServices.cs new file mode 100644 index 0000000..a865ad0 --- /dev/null +++ b/Neolution.Extensions.DataSeeding.UnitTests/Fakes/FakeServices.cs @@ -0,0 +1,86 @@ +namespace Neolution.Extensions.DataSeeding.UnitTests.Fakes +{ + /// + /// Fake singleton service interface. + /// + public interface IFakeSingletonService + { + /// + /// Gets the service identifier. + /// + string ServiceId { get; } + } + + /// + /// Fake scoped service interface. + /// + public interface IFakeScopedService + { + /// + /// Gets the service identifier. + /// + string ServiceId { get; } + } + + /// + /// Fake transient service interface. + /// + public interface IFakeTransientService + { + /// + /// Gets the service identifier. + /// + string ServiceId { get; } + } + + /// + /// Fake singleton service implementation. + /// + public class FakeSingletonService : IFakeSingletonService + { + /// + /// Initializes a new instance of the class. + /// + public FakeSingletonService() + { + this.ServiceId = System.Guid.NewGuid().ToString(); + } + + /// + public string ServiceId { get; } + } + + /// + /// Fake scoped service implementation. + /// + public class FakeScopedService : IFakeScopedService + { + /// + /// Initializes a new instance of the class. + /// + public FakeScopedService() + { + this.ServiceId = System.Guid.NewGuid().ToString(); + } + + /// + public string ServiceId { get; } + } + + /// + /// Fake transient service implementation. + /// + public class FakeTransientService : IFakeTransientService + { + /// + /// Initializes a new instance of the class. + /// + public FakeTransientService() + { + this.ServiceId = System.Guid.NewGuid().ToString(); + } + + /// + public string ServiceId { get; } + } +} diff --git a/Neolution.Extensions.DataSeeding.UnitTests/Fakes/MultiTenantSeeds/ApplicationSettingsSeed.cs b/Neolution.Extensions.DataSeeding.UnitTests/Fakes/MultiTenantSeeds/ApplicationSettingsSeed.cs index ee591ba..84a1399 100644 --- a/Neolution.Extensions.DataSeeding.UnitTests/Fakes/MultiTenantSeeds/ApplicationSettingsSeed.cs +++ b/Neolution.Extensions.DataSeeding.UnitTests/Fakes/MultiTenantSeeds/ApplicationSettingsSeed.cs @@ -12,19 +12,48 @@ public class ApplicationSettingsSeed : ISeed /// private readonly ILogger logger; + /// + /// The singleton service + /// + private readonly IFakeSingletonService singletonService; + + /// + /// The scoped service + /// + private readonly IFakeScopedService scopedService; + + /// + /// The transient service + /// + private readonly IFakeTransientService transientService; + /// /// Initializes a new instance of the class. /// /// The logger. - public ApplicationSettingsSeed(ILogger logger) + /// The singleton service. + /// The scoped service. + /// The transient service. + public ApplicationSettingsSeed( + ILogger logger, + IFakeSingletonService singletonService, + IFakeScopedService scopedService, + IFakeTransientService transientService) { this.logger = logger; + this.singletonService = singletonService; + this.scopedService = scopedService; + this.transientService = transientService; } /// public Task SeedAsync() { - this.logger.LogInformation("Seed() called"); + this.logger.LogInformation( + "Seed() called with Singleton: {SingletonId}, Scoped: {ScopedId}, Transient: {TransientId}", + this.singletonService.ServiceId, + this.scopedService.ServiceId, + this.transientService.ServiceId); return Task.CompletedTask; } } diff --git a/Neolution.Extensions.DataSeeding/Seeder.cs b/Neolution.Extensions.DataSeeding/Seeder.cs index 4bb8d1f..630c10f 100644 --- a/Neolution.Extensions.DataSeeding/Seeder.cs +++ b/Neolution.Extensions.DataSeeding/Seeder.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Neolution.Extensions.DataSeeding.Abstractions; using Neolution.Extensions.DataSeeding.Internal; @@ -17,6 +18,11 @@ internal sealed class Seeder : ISeeder /// private readonly ILogger logger; + /// + /// The service provider. + /// + private readonly IServiceProvider serviceProvider; + /// /// Initializes a new instance of the class. /// @@ -25,6 +31,7 @@ internal sealed class Seeder : ISeeder public Seeder(IServiceProvider serviceProvider, ILogger logger) { this.logger = logger; + this.serviceProvider = serviceProvider; Seeding.Instance.UseServiceProvider(serviceProvider); } @@ -59,9 +66,26 @@ public async Task SeedAsync() } this.logger.LogDebug("Start seeding..."); - foreach (var seed in sortedSeeds) + + // Create a scope to handle scoped dependencies + using (var scope = this.serviceProvider.CreateScope()) { - await seed.SeedAsync().ConfigureAwait(false); + // Resolve seeds within the scope to handle scoped dependencies + var scopedSeeds = new List(); + foreach (var seed in sortedSeeds) + { + var scopedSeed = scope.ServiceProvider.GetRequiredService(seed.GetType()) as ISeed; + if (scopedSeed != null) + { + scopedSeeds.Add(scopedSeed); + } + } + + // Execute the scoped seeds + foreach (var seed in scopedSeeds) + { + await seed.SeedAsync().ConfigureAwait(false); + } } this.logger.LogDebug("All seeds have been seeded!"); From 6eaf858dea57d1586cef75a208ec9d0767a90e11 Mon Sep 17 00:00:00 2001 From: Sandro Ciervo Date: Thu, 24 Jul 2025 14:21:11 +0200 Subject: [PATCH 10/16] Moves fake services to dedicated folder --- .../AbstractSeedTests.cs | 1 + .../BasicTests.cs | 1 + .../Fakes/FakeServices.cs | 86 ------------------- .../ApplicationSettingsSeed.cs | 1 + .../Fakes/Services/FakeScopedService.cs | 19 ++++ .../Fakes/Services/FakeSingletonService.cs | 19 ++++ .../Fakes/Services/FakeTransientService.cs | 19 ++++ .../Fakes/Services/IFakeScopedService.cs | 13 +++ .../Fakes/Services/IFakeSingletonService.cs | 13 +++ .../Fakes/Services/IFakeTransientService.cs | 13 +++ 10 files changed, 99 insertions(+), 86 deletions(-) delete mode 100644 Neolution.Extensions.DataSeeding.UnitTests/Fakes/FakeServices.cs create mode 100644 Neolution.Extensions.DataSeeding.UnitTests/Fakes/Services/FakeScopedService.cs create mode 100644 Neolution.Extensions.DataSeeding.UnitTests/Fakes/Services/FakeSingletonService.cs create mode 100644 Neolution.Extensions.DataSeeding.UnitTests/Fakes/Services/FakeTransientService.cs create mode 100644 Neolution.Extensions.DataSeeding.UnitTests/Fakes/Services/IFakeScopedService.cs create mode 100644 Neolution.Extensions.DataSeeding.UnitTests/Fakes/Services/IFakeSingletonService.cs create mode 100644 Neolution.Extensions.DataSeeding.UnitTests/Fakes/Services/IFakeTransientService.cs diff --git a/Neolution.Extensions.DataSeeding.UnitTests/AbstractSeedTests.cs b/Neolution.Extensions.DataSeeding.UnitTests/AbstractSeedTests.cs index df4a8be..0b50d59 100644 --- a/Neolution.Extensions.DataSeeding.UnitTests/AbstractSeedTests.cs +++ b/Neolution.Extensions.DataSeeding.UnitTests/AbstractSeedTests.cs @@ -6,6 +6,7 @@ using Neolution.Extensions.DataSeeding.Abstractions; using Neolution.Extensions.DataSeeding.UnitTests.Fakes; using Neolution.Extensions.DataSeeding.UnitTests.Fakes.MultiTenantSeeds; + using Neolution.Extensions.DataSeeding.UnitTests.Fakes.Services; using Shouldly; using Xunit; using Xunit.Abstractions; diff --git a/Neolution.Extensions.DataSeeding.UnitTests/BasicTests.cs b/Neolution.Extensions.DataSeeding.UnitTests/BasicTests.cs index b1c22fc..71a9f5e 100644 --- a/Neolution.Extensions.DataSeeding.UnitTests/BasicTests.cs +++ b/Neolution.Extensions.DataSeeding.UnitTests/BasicTests.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Neolution.Extensions.DataSeeding.UnitTests.Fakes; + using Neolution.Extensions.DataSeeding.UnitTests.Fakes.Services; using Shouldly; using Xunit; using Xunit.Abstractions; diff --git a/Neolution.Extensions.DataSeeding.UnitTests/Fakes/FakeServices.cs b/Neolution.Extensions.DataSeeding.UnitTests/Fakes/FakeServices.cs deleted file mode 100644 index a865ad0..0000000 --- a/Neolution.Extensions.DataSeeding.UnitTests/Fakes/FakeServices.cs +++ /dev/null @@ -1,86 +0,0 @@ -namespace Neolution.Extensions.DataSeeding.UnitTests.Fakes -{ - /// - /// Fake singleton service interface. - /// - public interface IFakeSingletonService - { - /// - /// Gets the service identifier. - /// - string ServiceId { get; } - } - - /// - /// Fake scoped service interface. - /// - public interface IFakeScopedService - { - /// - /// Gets the service identifier. - /// - string ServiceId { get; } - } - - /// - /// Fake transient service interface. - /// - public interface IFakeTransientService - { - /// - /// Gets the service identifier. - /// - string ServiceId { get; } - } - - /// - /// Fake singleton service implementation. - /// - public class FakeSingletonService : IFakeSingletonService - { - /// - /// Initializes a new instance of the class. - /// - public FakeSingletonService() - { - this.ServiceId = System.Guid.NewGuid().ToString(); - } - - /// - public string ServiceId { get; } - } - - /// - /// Fake scoped service implementation. - /// - public class FakeScopedService : IFakeScopedService - { - /// - /// Initializes a new instance of the class. - /// - public FakeScopedService() - { - this.ServiceId = System.Guid.NewGuid().ToString(); - } - - /// - public string ServiceId { get; } - } - - /// - /// Fake transient service implementation. - /// - public class FakeTransientService : IFakeTransientService - { - /// - /// Initializes a new instance of the class. - /// - public FakeTransientService() - { - this.ServiceId = System.Guid.NewGuid().ToString(); - } - - /// - public string ServiceId { get; } - } -} diff --git a/Neolution.Extensions.DataSeeding.UnitTests/Fakes/MultiTenantSeeds/ApplicationSettingsSeed.cs b/Neolution.Extensions.DataSeeding.UnitTests/Fakes/MultiTenantSeeds/ApplicationSettingsSeed.cs index 84a1399..6e0e23a 100644 --- a/Neolution.Extensions.DataSeeding.UnitTests/Fakes/MultiTenantSeeds/ApplicationSettingsSeed.cs +++ b/Neolution.Extensions.DataSeeding.UnitTests/Fakes/MultiTenantSeeds/ApplicationSettingsSeed.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Neolution.Extensions.DataSeeding.Abstractions; + using Neolution.Extensions.DataSeeding.UnitTests.Fakes.Services; /// public class ApplicationSettingsSeed : ISeed diff --git a/Neolution.Extensions.DataSeeding.UnitTests/Fakes/Services/FakeScopedService.cs b/Neolution.Extensions.DataSeeding.UnitTests/Fakes/Services/FakeScopedService.cs new file mode 100644 index 0000000..4a55076 --- /dev/null +++ b/Neolution.Extensions.DataSeeding.UnitTests/Fakes/Services/FakeScopedService.cs @@ -0,0 +1,19 @@ +namespace Neolution.Extensions.DataSeeding.UnitTests.Fakes.Services +{ + /// + /// Fake scoped service implementation. + /// + public class FakeScopedService : IFakeScopedService + { + /// + /// Initializes a new instance of the class. + /// + public FakeScopedService() + { + this.ServiceId = System.Guid.NewGuid().ToString(); + } + + /// + public string ServiceId { get; } + } +} diff --git a/Neolution.Extensions.DataSeeding.UnitTests/Fakes/Services/FakeSingletonService.cs b/Neolution.Extensions.DataSeeding.UnitTests/Fakes/Services/FakeSingletonService.cs new file mode 100644 index 0000000..0eb323a --- /dev/null +++ b/Neolution.Extensions.DataSeeding.UnitTests/Fakes/Services/FakeSingletonService.cs @@ -0,0 +1,19 @@ +namespace Neolution.Extensions.DataSeeding.UnitTests.Fakes.Services +{ + /// + /// Fake singleton service implementation. + /// + public class FakeSingletonService : IFakeSingletonService + { + /// + /// Initializes a new instance of the class. + /// + public FakeSingletonService() + { + this.ServiceId = System.Guid.NewGuid().ToString(); + } + + /// + public string ServiceId { get; } + } +} diff --git a/Neolution.Extensions.DataSeeding.UnitTests/Fakes/Services/FakeTransientService.cs b/Neolution.Extensions.DataSeeding.UnitTests/Fakes/Services/FakeTransientService.cs new file mode 100644 index 0000000..8b0bc7f --- /dev/null +++ b/Neolution.Extensions.DataSeeding.UnitTests/Fakes/Services/FakeTransientService.cs @@ -0,0 +1,19 @@ +namespace Neolution.Extensions.DataSeeding.UnitTests.Fakes.Services +{ + /// + /// Fake transient service implementation. + /// + public class FakeTransientService : IFakeTransientService + { + /// + /// Initializes a new instance of the class. + /// + public FakeTransientService() + { + this.ServiceId = System.Guid.NewGuid().ToString(); + } + + /// + public string ServiceId { get; } + } +} diff --git a/Neolution.Extensions.DataSeeding.UnitTests/Fakes/Services/IFakeScopedService.cs b/Neolution.Extensions.DataSeeding.UnitTests/Fakes/Services/IFakeScopedService.cs new file mode 100644 index 0000000..97c9d0f --- /dev/null +++ b/Neolution.Extensions.DataSeeding.UnitTests/Fakes/Services/IFakeScopedService.cs @@ -0,0 +1,13 @@ +namespace Neolution.Extensions.DataSeeding.UnitTests.Fakes.Services +{ + /// + /// Fake scoped service interface. + /// + public interface IFakeScopedService + { + /// + /// Gets the service identifier. + /// + string ServiceId { get; } + } +} diff --git a/Neolution.Extensions.DataSeeding.UnitTests/Fakes/Services/IFakeSingletonService.cs b/Neolution.Extensions.DataSeeding.UnitTests/Fakes/Services/IFakeSingletonService.cs new file mode 100644 index 0000000..dab6369 --- /dev/null +++ b/Neolution.Extensions.DataSeeding.UnitTests/Fakes/Services/IFakeSingletonService.cs @@ -0,0 +1,13 @@ +namespace Neolution.Extensions.DataSeeding.UnitTests.Fakes.Services +{ + /// + /// Fake singleton service interface. + /// + public interface IFakeSingletonService + { + /// + /// Gets the service identifier. + /// + string ServiceId { get; } + } +} diff --git a/Neolution.Extensions.DataSeeding.UnitTests/Fakes/Services/IFakeTransientService.cs b/Neolution.Extensions.DataSeeding.UnitTests/Fakes/Services/IFakeTransientService.cs new file mode 100644 index 0000000..52cc476 --- /dev/null +++ b/Neolution.Extensions.DataSeeding.UnitTests/Fakes/Services/IFakeTransientService.cs @@ -0,0 +1,13 @@ +namespace Neolution.Extensions.DataSeeding.UnitTests.Fakes.Services +{ + /// + /// Fake transient service interface. + /// + public interface IFakeTransientService + { + /// + /// Gets the service identifier. + /// + string ServiceId { get; } + } +} From 16f501225ee387af4f3a4d46b82d05ea86003231 Mon Sep 17 00:00:00 2001 From: Sandro Ciervo Date: Thu, 24 Jul 2025 14:22:04 +0200 Subject: [PATCH 11/16] Simplifies seed retrieval logic Refactors the code to directly cast the resolved service to ISeed within the 'if' condition. --- Neolution.Extensions.DataSeeding/Seeder.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Neolution.Extensions.DataSeeding/Seeder.cs b/Neolution.Extensions.DataSeeding/Seeder.cs index 630c10f..d1095e9 100644 --- a/Neolution.Extensions.DataSeeding/Seeder.cs +++ b/Neolution.Extensions.DataSeeding/Seeder.cs @@ -74,8 +74,7 @@ public async Task SeedAsync() var scopedSeeds = new List(); foreach (var seed in sortedSeeds) { - var scopedSeed = scope.ServiceProvider.GetRequiredService(seed.GetType()) as ISeed; - if (scopedSeed != null) + if (scope.ServiceProvider.GetRequiredService(seed.GetType()) is ISeed scopedSeed) { scopedSeeds.Add(scopedSeed); } From 202e5b0bf0651c44266274ad326999bcb5dc81c2 Mon Sep 17 00:00:00 2001 From: Sandro Ciervo Date: Thu, 24 Jul 2025 17:20:30 +0200 Subject: [PATCH 12/16] feat: enable scoped service injection in seeds for console applications - Fix ObjectDisposedException when seeds use scoped dependencies like UserManager - Use temporary scope for dependency analysis to handle scoped services - Resolve fresh seed instances within execution scope instead of caching - Register seeds as both concrete types and interfaces for proper DI resolution - Add comprehensive tests for scope disposal scenarios and scoped service dependencies Resolves issue where seeds with scoped dependencies (e.g., UserManager) would throw ObjectDisposedException because they were resolved from a disposed scope during dependency analysis phase and then executed later. Changes: - Seeding.UseServiceProvider: Use temporary scope for seed resolution during analysis - Seeder.SeedAsync: Create fresh scope and resolve fresh seed instances during execution - ServiceCollectionExtensions: Register seeds with .AsSelf() for concrete type resolution - Add ScopeDisposalTests to verify fix and prevent regression - Add FakeScopedServiceWithDependency to simulate UserManager-like scenarios - Add ScopedServiceWithDependencySeed test seed for integration testing Breaking changes: None - fully backward compatible --- .../AbstractSeedTests.cs | 3 + .../BasicTests.cs | 3 + .../ScopedServiceWithDependencySeed.cs | 47 +++++++++ .../FakeScopedServiceWithDependency.cs | 47 +++++++++ .../ScopeDisposalTests.cs | 98 +++++++++++++++++++ Neolution.Extensions.DataSeeding/Seeder.cs | 20 ++-- Neolution.Extensions.DataSeeding/Seeding.cs | 10 +- .../ServiceCollectionExtensions.cs | 2 + 8 files changed, 214 insertions(+), 16 deletions(-) create mode 100644 Neolution.Extensions.DataSeeding.UnitTests/Fakes/MultiTenantSeeds/ScopedServiceWithDependencySeed.cs create mode 100644 Neolution.Extensions.DataSeeding.UnitTests/Fakes/Services/FakeScopedServiceWithDependency.cs create mode 100644 Neolution.Extensions.DataSeeding.UnitTests/ScopeDisposalTests.cs diff --git a/Neolution.Extensions.DataSeeding.UnitTests/AbstractSeedTests.cs b/Neolution.Extensions.DataSeeding.UnitTests/AbstractSeedTests.cs index 0b50d59..b0a69b9 100644 --- a/Neolution.Extensions.DataSeeding.UnitTests/AbstractSeedTests.cs +++ b/Neolution.Extensions.DataSeeding.UnitTests/AbstractSeedTests.cs @@ -103,6 +103,9 @@ private IServiceCollection CreateServiceCollection() services.AddScoped(); services.AddTransient(); + // Register the scoped service with dependency to test UserManager-like scenarios + services.AddScoped(); + return services; } } diff --git a/Neolution.Extensions.DataSeeding.UnitTests/BasicTests.cs b/Neolution.Extensions.DataSeeding.UnitTests/BasicTests.cs index 71a9f5e..6d3b056 100644 --- a/Neolution.Extensions.DataSeeding.UnitTests/BasicTests.cs +++ b/Neolution.Extensions.DataSeeding.UnitTests/BasicTests.cs @@ -61,6 +61,9 @@ private IServiceCollection CreateServiceCollection() services.AddScoped(); services.AddTransient(); + // Register the scoped service with dependency to test UserManager-like scenarios + services.AddScoped(); + return services; } } diff --git a/Neolution.Extensions.DataSeeding.UnitTests/Fakes/MultiTenantSeeds/ScopedServiceWithDependencySeed.cs b/Neolution.Extensions.DataSeeding.UnitTests/Fakes/MultiTenantSeeds/ScopedServiceWithDependencySeed.cs new file mode 100644 index 0000000..6d6e98b --- /dev/null +++ b/Neolution.Extensions.DataSeeding.UnitTests/Fakes/MultiTenantSeeds/ScopedServiceWithDependencySeed.cs @@ -0,0 +1,47 @@ +namespace Neolution.Extensions.DataSeeding.UnitTests.Fakes.MultiTenantSeeds +{ + using System.Threading.Tasks; + using Microsoft.Extensions.Logging; + using Neolution.Extensions.DataSeeding.Abstractions; + using Neolution.Extensions.DataSeeding.UnitTests.Fakes.Services; + + /// + /// A seed that tests scoped service dependencies similar to UserManager scenario. + /// + public class ScopedServiceWithDependencySeed : ISeed + { + /// + /// The logger + /// + private readonly ILogger logger; + + /// + /// The scoped service with dependency + /// + private readonly IFakeScopedServiceWithDependency scopedServiceWithDependency; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// The scoped service with dependency. + public ScopedServiceWithDependencySeed( + ILogger logger, + IFakeScopedServiceWithDependency scopedServiceWithDependency) + { + this.logger = logger; + this.scopedServiceWithDependency = scopedServiceWithDependency; + } + + /// + public async Task SeedAsync() + { + this.logger.LogInformation("Starting scoped service with dependency seed"); + + // This should trigger ObjectDisposedException if scope is disposed + var result = await this.scopedServiceWithDependency.PerformScopedOperationAsync(); + + this.logger.LogInformation("Scoped operation result: {Result}", result); + } + } +} diff --git a/Neolution.Extensions.DataSeeding.UnitTests/Fakes/Services/FakeScopedServiceWithDependency.cs b/Neolution.Extensions.DataSeeding.UnitTests/Fakes/Services/FakeScopedServiceWithDependency.cs new file mode 100644 index 0000000..6766686 --- /dev/null +++ b/Neolution.Extensions.DataSeeding.UnitTests/Fakes/Services/FakeScopedServiceWithDependency.cs @@ -0,0 +1,47 @@ +namespace Neolution.Extensions.DataSeeding.UnitTests.Fakes.Services +{ + using System; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + + /// + /// Interface for a scoped service that depends on other scoped services. + /// + public interface IFakeScopedServiceWithDependency + { + /// + /// Performs an async operation that accesses scoped dependencies. + /// + /// A task representing the async operation. + Task PerformScopedOperationAsync(); + } + + /// + /// A scoped service that depends on other scoped services, similar to UserManager. + /// + public class FakeScopedServiceWithDependency : IFakeScopedServiceWithDependency + { + private readonly IServiceProvider serviceProvider; + + /// + /// Initializes a new instance of the class. + /// + /// The service provider. + public FakeScopedServiceWithDependency(IServiceProvider serviceProvider) + { + this.serviceProvider = serviceProvider; + } + + /// + /// Performs an async operation that accesses scoped dependencies. + /// + /// A task representing the async operation. + public async Task PerformScopedOperationAsync() + { + // This simulates UserManager.CreateAsync accessing other scoped services + var scopedService = this.serviceProvider.GetRequiredService(); + await Task.Delay(1); // Simulate async work + return $"Operation completed with {scopedService.ServiceId}"; + } + } +} diff --git a/Neolution.Extensions.DataSeeding.UnitTests/ScopeDisposalTests.cs b/Neolution.Extensions.DataSeeding.UnitTests/ScopeDisposalTests.cs new file mode 100644 index 0000000..4df9c95 --- /dev/null +++ b/Neolution.Extensions.DataSeeding.UnitTests/ScopeDisposalTests.cs @@ -0,0 +1,98 @@ +namespace Neolution.Extensions.DataSeeding.UnitTests +{ + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Logging; + using Neolution.Extensions.DataSeeding.Abstractions; + using Neolution.Extensions.DataSeeding.UnitTests.Fakes.Services; + using Shouldly; + using Xunit; + using Xunit.Abstractions; + + /// + /// Tests that demonstrate the ObjectDisposedException issue and verify the fix. + /// + public class ScopeDisposalTests + { + /// + /// The test output helper + /// + private readonly ITestOutputHelper testOutputHelper; + + /// + /// Initializes a new instance of the class. + /// + /// The test output helper. + public ScopeDisposalTests(ITestOutputHelper testOutputHelper) + { + this.testOutputHelper = testOutputHelper; + } + + /// + /// Tests that scoped services accessed after scope disposal throw ObjectDisposedException. + /// This demonstrates the problem we fixed. + /// + [Fact] + public void ScopedService_AccessedAfterScopeDisposal_ThrowsObjectDisposedException() + { + // Arrange + var services = this.CreateServiceCollection(); + var serviceProvider = services.BuildServiceProvider(); + + IFakeScopedServiceWithDependency scopedService; + + // Create and dispose a scope (simulating the old problematic approach) + using (var scope = serviceProvider.CreateScope()) + { + scopedService = scope.ServiceProvider.GetRequiredService(); + } // Scope is disposed here + + // Act & Assert - This should throw because the scope is disposed + var exception = Should.Throw(async () => + { + await scopedService.PerformScopedOperationAsync(); + }); + + exception.ShouldNotBeNull(); + } + + /// + /// Tests that the current seeding implementation works correctly with scoped services. + /// This verifies our fix is working. + /// + /// A task representing the asynchronous test operation. + [Fact] + public async Task CurrentSeedingImplementation_WithScopedServices_WorksCorrectly() + { + // Arrange + var services = this.CreateServiceCollection(); + services.AddDataSeeding(typeof(ScopeDisposalTests).Assembly); + var serviceProvider = services.BuildServiceProvider(); + + // Act & Assert - This should work without throwing + var seeder = serviceProvider.GetRequiredService(); + await seeder.SeedAsync(); + + // If we get here, the scoped services worked correctly + seeder.ShouldNotBeNull(); + } + + /// + /// Creates the service collection. + /// + /// The . + private IServiceCollection CreateServiceCollection() + { + var services = new ServiceCollection(); + services.AddLogging(builder => builder.AddXUnit(this.testOutputHelper)); + + // Register all fake services that seeds might need + services.AddSingleton(); + services.AddScoped(); + services.AddTransient(); + services.AddScoped(); + + return services; + } + } +} diff --git a/Neolution.Extensions.DataSeeding/Seeder.cs b/Neolution.Extensions.DataSeeding/Seeder.cs index d1095e9..9f123ea 100644 --- a/Neolution.Extensions.DataSeeding/Seeder.cs +++ b/Neolution.Extensions.DataSeeding/Seeder.cs @@ -67,23 +67,17 @@ public async Task SeedAsync() this.logger.LogDebug("Start seeding..."); - // Create a scope to handle scoped dependencies + // Create a scope to handle scoped dependencies and resolve fresh seeds using (var scope = this.serviceProvider.CreateScope()) { - // Resolve seeds within the scope to handle scoped dependencies - var scopedSeeds = new List(); + // Resolve fresh instances of seeds within the scope to handle scoped dependencies foreach (var seed in sortedSeeds) { - if (scope.ServiceProvider.GetRequiredService(seed.GetType()) is ISeed scopedSeed) - { - scopedSeeds.Add(scopedSeed); - } - } - - // Execute the scoped seeds - foreach (var seed in scopedSeeds) - { - await seed.SeedAsync().ConfigureAwait(false); + // Get a fresh instance of the seed from the scoped service provider + // Use the concrete type to ensure proper resolution + var seedType = seed.GetType(); + var scopedSeed = (ISeed)scope.ServiceProvider.GetRequiredService(seedType); + await scopedSeed.SeedAsync().ConfigureAwait(false); } } diff --git a/Neolution.Extensions.DataSeeding/Seeding.cs b/Neolution.Extensions.DataSeeding/Seeding.cs index 7c39ebe..f7f661f 100644 --- a/Neolution.Extensions.DataSeeding/Seeding.cs +++ b/Neolution.Extensions.DataSeeding/Seeding.cs @@ -74,9 +74,13 @@ internal void UseServiceProvider(IServiceProvider serviceProvider) logger.LogTrace("Assembly full name: '{SeedsAssemblyFullName}'", this.seedsAssembly.FullName); } - // Resolve all seeds that are registered - this.Seeds = serviceProvider.GetServices().ToList(); - this.seeds = serviceProvider.GetServices().ToList(); + // Resolve all seeds that are registered (temporarily for dependency analysis) + // Use a temporary scope to handle scoped dependencies during analysis + using (var tempScope = serviceProvider.CreateScope()) + { + this.Seeds = tempScope.ServiceProvider.GetServices().ToList(); + this.seeds = tempScope.ServiceProvider.GetServices().ToList(); + } // Dispose the temporary scope - seeds will be resolved fresh during execution logger.LogDebug("{SeedsCount} seeds have been found and loaded", this.Seeds.Count + this.seeds.Count); logger.LogTrace("{SeedCount} ISeed implementations found", this.Seeds.Count); diff --git a/Neolution.Extensions.DataSeeding/ServiceCollectionExtensions.cs b/Neolution.Extensions.DataSeeding/ServiceCollectionExtensions.cs index 96db566..fff1e9d 100644 --- a/Neolution.Extensions.DataSeeding/ServiceCollectionExtensions.cs +++ b/Neolution.Extensions.DataSeeding/ServiceCollectionExtensions.cs @@ -23,6 +23,7 @@ public static void AddDataSeeding(this IServiceCollection services, Assembly ass services.Scan(scan => scan .FromAssemblies(assembly) .AddClasses(classes => classes.AssignableTo()) + .AsSelf() .AsImplementedInterfaces() .WithTransientLifetime()); @@ -30,6 +31,7 @@ public static void AddDataSeeding(this IServiceCollection services, Assembly ass services.Scan(scan => scan .FromAssemblies(assembly) .AddClasses(classes => classes.AssignableTo()) + .AsSelf() .As() .WithTransientLifetime()); } From a45da2ce8116182108f348068316309b2269f808 Mon Sep 17 00:00:00 2001 From: Sandro Ciervo Date: Thu, 24 Jul 2025 18:00:12 +0200 Subject: [PATCH 13/16] Adds scoped service lifetime handling tests --- .../FakeScopedServiceWithDependency.cs | 15 +- .../IFakeScopedServiceWithDependency.cs | 16 ++ .../ScopedServiceLifetimeTests.cs | 111 +++++++++++++ .../SeedingWorkflowComponentTests.cs | 146 ++++++++++++++++++ .../WorkflowTestSeed.cs | 58 +++++++ 5 files changed, 334 insertions(+), 12 deletions(-) create mode 100644 Neolution.Extensions.DataSeeding.UnitTests/Fakes/Services/IFakeScopedServiceWithDependency.cs create mode 100644 Neolution.Extensions.DataSeeding.UnitTests/ScopedServiceLifetimeTests.cs create mode 100644 Neolution.Extensions.DataSeeding.UnitTests/SeedingWorkflowComponentTests.cs create mode 100644 Neolution.Extensions.DataSeeding.UnitTests/WorkflowTestSeed.cs diff --git a/Neolution.Extensions.DataSeeding.UnitTests/Fakes/Services/FakeScopedServiceWithDependency.cs b/Neolution.Extensions.DataSeeding.UnitTests/Fakes/Services/FakeScopedServiceWithDependency.cs index 6766686..cc6c66c 100644 --- a/Neolution.Extensions.DataSeeding.UnitTests/Fakes/Services/FakeScopedServiceWithDependency.cs +++ b/Neolution.Extensions.DataSeeding.UnitTests/Fakes/Services/FakeScopedServiceWithDependency.cs @@ -4,23 +4,14 @@ using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; - /// - /// Interface for a scoped service that depends on other scoped services. - /// - public interface IFakeScopedServiceWithDependency - { - /// - /// Performs an async operation that accesses scoped dependencies. - /// - /// A task representing the async operation. - Task PerformScopedOperationAsync(); - } - /// /// A scoped service that depends on other scoped services, similar to UserManager. /// public class FakeScopedServiceWithDependency : IFakeScopedServiceWithDependency { + /// + /// The service provider. + /// private readonly IServiceProvider serviceProvider; /// diff --git a/Neolution.Extensions.DataSeeding.UnitTests/Fakes/Services/IFakeScopedServiceWithDependency.cs b/Neolution.Extensions.DataSeeding.UnitTests/Fakes/Services/IFakeScopedServiceWithDependency.cs new file mode 100644 index 0000000..17e0181 --- /dev/null +++ b/Neolution.Extensions.DataSeeding.UnitTests/Fakes/Services/IFakeScopedServiceWithDependency.cs @@ -0,0 +1,16 @@ +namespace Neolution.Extensions.DataSeeding.UnitTests.Fakes.Services +{ + using System.Threading.Tasks; + + /// + /// Interface for a scoped service that depends on other scoped services. + /// + public interface IFakeScopedServiceWithDependency + { + /// + /// Performs an async operation that accesses scoped dependencies. + /// + /// A task representing the async operation. + Task PerformScopedOperationAsync(); + } +} diff --git a/Neolution.Extensions.DataSeeding.UnitTests/ScopedServiceLifetimeTests.cs b/Neolution.Extensions.DataSeeding.UnitTests/ScopedServiceLifetimeTests.cs new file mode 100644 index 0000000..992239d --- /dev/null +++ b/Neolution.Extensions.DataSeeding.UnitTests/ScopedServiceLifetimeTests.cs @@ -0,0 +1,111 @@ +namespace Neolution.Extensions.DataSeeding.UnitTests +{ + using System; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Logging; + using Neolution.Extensions.DataSeeding.Abstractions; + using Neolution.Extensions.DataSeeding.UnitTests.Fakes.Services; + using Shouldly; + using Xunit; + using Xunit.Abstractions; + + /// + /// Tests to ensure proper service lifetime handling during seeding operations. + /// These tests prevent regressions related to scoped service injection. + /// + public class ScopedServiceLifetimeTests + { + /// + /// The test output helper. + /// + private readonly ITestOutputHelper testOutputHelper; + + /// + /// Initializes a new instance of the class. + /// + /// The test output helper. + public ScopedServiceLifetimeTests(ITestOutputHelper testOutputHelper) + { + this.testOutputHelper = testOutputHelper; + } + + /// + /// Tests that seeding operations don't leak scoped services outside their scope. + /// + /// A task representing the asynchronous test operation. + [Fact] + public async Task SeedingOperations_DoNotLeakScopedServices() + { + // Arrange + var services = this.CreateServiceCollection(); + services.AddDataSeeding(typeof(ScopedServiceLifetimeTests).Assembly); + var serviceProvider = services.BuildServiceProvider(); + var seeder = serviceProvider.GetRequiredService(); + + // Act + await seeder.SeedAsync(); + + // Assert - After seeding, we should not be able to access scoped services from root provider + Should.Throw(() => + serviceProvider.GetRequiredService()); + } + + /// + /// Tests that dependency analysis does not cache scoped service instances. + /// + [Fact] + public void DependencyAnalysis_DoesNotCacheScopedServiceInstances() + { + // Arrange + var services = this.CreateServiceCollection(); + services.AddDataSeeding(typeof(ScopedServiceLifetimeTests).Assembly); + var serviceProvider = services.BuildServiceProvider(); + + // Act - Just creating the seeder should trigger dependency analysis + serviceProvider.GetRequiredService(); + + // Assert - We should still be able to create new scoped services + using var scope = serviceProvider.CreateScope(); + var scopedService = scope.ServiceProvider.GetRequiredService(); + scopedService.ShouldNotBeNull(); + scopedService.ServiceId.ShouldNotBeEmpty(); + } + + /// + /// Tests that the root cause error (cannot resolve scoped service from root) does not occur. + /// + [Fact] + public void SeedRegistration_DoesNotTriggerScopedServiceRootResolutionError() + { + // Arrange & Act - This should not throw the root cause error: + // "Cannot resolve 'IEnumerable' from root provider because it requires scoped service" + var services = this.CreateServiceCollection(); + + Should.NotThrow(() => + { + services.AddDataSeeding(typeof(ScopedServiceLifetimeTests).Assembly); + var serviceProvider = services.BuildServiceProvider(); + serviceProvider.GetRequiredService(); + }); + } + + /// + /// Creates the service collection with all required dependencies. + /// + /// The . + private IServiceCollection CreateServiceCollection() + { + var services = new ServiceCollection(); + services.AddLogging(builder => builder.AddXUnit(this.testOutputHelper)); + + // Register services with different lifetimes + services.AddSingleton(); + services.AddScoped(); + services.AddTransient(); + services.AddScoped(); + + return services; + } + } +} diff --git a/Neolution.Extensions.DataSeeding.UnitTests/SeedingWorkflowComponentTests.cs b/Neolution.Extensions.DataSeeding.UnitTests/SeedingWorkflowComponentTests.cs new file mode 100644 index 0000000..9a980e7 --- /dev/null +++ b/Neolution.Extensions.DataSeeding.UnitTests/SeedingWorkflowComponentTests.cs @@ -0,0 +1,146 @@ +namespace Neolution.Extensions.DataSeeding.UnitTests +{ + using System; + using System.Linq; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Logging; + using Neolution.Extensions.DataSeeding.Abstractions; + using Neolution.Extensions.DataSeeding.UnitTests.Fakes.Services; + using Shouldly; + using Xunit; + using Xunit.Abstractions; + + /// + /// Component tests that verify the complete seeding workflow with various service lifetimes. + /// These tests ensure the entire seeding pipeline works correctly with scoped dependencies. + /// + public class SeedingWorkflowComponentTests + { + /// + /// The test output helper + /// + private readonly ITestOutputHelper testOutputHelper; + + /// + /// Initializes a new instance of the class. + /// + /// The test output helper. + public SeedingWorkflowComponentTests(ITestOutputHelper testOutputHelper) + { + this.testOutputHelper = testOutputHelper; + } + + /// + /// Tests the complete workflow: registration → dependency analysis → execution with scoped services. + /// + /// A task representing the asynchronous test operation. + [Fact] + public async Task CompleteWorkflow_WithScopedServices_ExecutesSuccessfully() + { + // Arrange + var services = this.CreateServiceCollection(); + services.AddDataSeeding(typeof(SeedingWorkflowComponentTests).Assembly); + var serviceProvider = services.BuildServiceProvider(); + + // Act & Assert - This tests the complete workflow: + // 1. Service registration (AddDataSeeding) + // 2. Dependency analysis (UseServiceProvider in Seeding) + // 3. Seed execution (SeedAsync in Seeder) + var seeder = serviceProvider.GetRequiredService(); + await seeder.SeedAsync(); + + // If we reach here, the entire workflow succeeded + seeder.ShouldNotBeNull(); + } + + /// + /// Tests that seeds are resolved with correct concrete types during execution. + /// + /// A task representing the asynchronous test operation. + [Fact] + public async Task SeedExecution_ResolvesSeedsWithCorrectConcreteTypes() + { + // Arrange + var services = this.CreateServiceCollection(); + services.AddDataSeeding(typeof(SeedingWorkflowComponentTests).Assembly); + var serviceProvider = services.BuildServiceProvider(); + + // Act + var seeder = serviceProvider.GetRequiredService(); + + // Verify that specific seeds can be resolved by their concrete types + var iSeedImplementations = serviceProvider.GetServices(); + var concreteSeedTypes = iSeedImplementations.Select(s => s.GetType()).ToList(); + + // Assert + concreteSeedTypes.ShouldContain(typeof(WorkflowTestSeed)); + concreteSeedTypes.ShouldNotBeEmpty(); + + // Verify execution works + await seeder.SeedAsync(); + } + + /// + /// Tests that service registration includes both interface and concrete type registrations. + /// + [Fact] + public void ServiceRegistration_IncludesBothInterfaceAndConcreteTypes() + { + // Arrange + var services = this.CreateServiceCollection(); + services.AddDataSeeding(typeof(SeedingWorkflowComponentTests).Assembly); + var serviceProvider = services.BuildServiceProvider(); + + // Act & Assert - Seeds should be registered as both ISeed and their concrete types + var workflowSeedAsInterface = serviceProvider.GetServices() + .FirstOrDefault(s => s.GetType() == typeof(WorkflowTestSeed)); + var workflowSeedAsConcrete = serviceProvider.GetService(); + + workflowSeedAsInterface.ShouldNotBeNull(); + workflowSeedAsConcrete.ShouldNotBeNull(); + + // Seeds are transient, so instances will be different, but both registrations should work + workflowSeedAsInterface.ShouldBeOfType(); + workflowSeedAsConcrete.ShouldBeOfType(); + } + + /// + /// Tests that dependency analysis can handle seeds with complex dependency chains. + /// + /// A task representing the asynchronous test operation. + [Fact] + public async Task DependencyAnalysis_HandlesComplexDependencyChains() + { + // Arrange + var services = this.CreateServiceCollection(); + services.AddDataSeeding(typeof(SeedingWorkflowComponentTests).Assembly); + var serviceProvider = services.BuildServiceProvider(); + + // Act - This should work even with seeds that have multiple scoped dependencies + var seeder = serviceProvider.GetRequiredService(); + await seeder.SeedAsync(); + + // Assert - If we get here, dependency analysis and execution both succeeded + seeder.ShouldNotBeNull(); + } + + /// + /// Creates the service collection with comprehensive service registrations. + /// + /// The . + private IServiceCollection CreateServiceCollection() + { + var services = new ServiceCollection(); + services.AddLogging(builder => builder.AddXUnit(this.testOutputHelper).SetMinimumLevel(LogLevel.Trace)); + + // Register all service lifetimes to test comprehensive scenarios + services.AddSingleton(); + services.AddScoped(); + services.AddTransient(); + services.AddScoped(); + + return services; + } + } +} diff --git a/Neolution.Extensions.DataSeeding.UnitTests/WorkflowTestSeed.cs b/Neolution.Extensions.DataSeeding.UnitTests/WorkflowTestSeed.cs new file mode 100644 index 0000000..fb8d83a --- /dev/null +++ b/Neolution.Extensions.DataSeeding.UnitTests/WorkflowTestSeed.cs @@ -0,0 +1,58 @@ +namespace Neolution.Extensions.DataSeeding.UnitTests +{ + using System.Threading.Tasks; + using Microsoft.Extensions.Logging; + using Neolution.Extensions.DataSeeding.Abstractions; + using Neolution.Extensions.DataSeeding.UnitTests.Fakes.Services; + + /// + /// Test seed for workflow integration testing. + /// + public class WorkflowTestSeed : ISeed + { + /// + /// The logger. + /// + private readonly ILogger logger; + + /// + /// The scoped service. + /// + private readonly IFakeScopedService scopedService; + + /// + /// The scoped service with dependency. + /// + private readonly IFakeScopedServiceWithDependency scopedServiceWithDependency; + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// The scoped service. + /// The scoped service with dependency. + public WorkflowTestSeed( + ILogger logger, + IFakeScopedService scopedService, + IFakeScopedServiceWithDependency scopedServiceWithDependency) + { + this.logger = logger; + this.scopedService = scopedService; + this.scopedServiceWithDependency = scopedServiceWithDependency; + } + + /// + /// Seeds the data asynchronously. + /// + /// A task representing the asynchronous operation. + public async Task SeedAsync() + { + this.logger.LogInformation("Executing workflow test seed with scoped services"); + + // Test both scoped services + this.logger.LogInformation("Scoped service ID: {ServiceId}", this.scopedService.ServiceId); + var result = await this.scopedServiceWithDependency.PerformScopedOperationAsync(); + this.logger.LogInformation("Scoped operation result: {Result}", result); + } + } +} From 0d33ce1f7eca347df9752c401098b6fbc5db736f Mon Sep 17 00:00:00 2001 From: Sandro Ciervo Date: Sun, 27 Jul 2025 03:11:31 +0200 Subject: [PATCH 14/16] fix: Enable scoped service injection in seeds for console applications Resolves ObjectDisposedException when injecting scoped services like UserManager into seeds in console applications. --- .../AbstractSeedTests.cs | 4 ++-- .../BasicTests.cs | 2 +- .../ScopedServiceLifetimeTests.cs | 15 +++++++++------ Neolution.Extensions.DataSeeding/Seeder.cs | 2 +- Neolution.Extensions.DataSeeding/Seeding.cs | 4 ++-- .../ServiceCollectionExtensions.cs | 2 +- 6 files changed, 16 insertions(+), 13 deletions(-) diff --git a/Neolution.Extensions.DataSeeding.UnitTests/AbstractSeedTests.cs b/Neolution.Extensions.DataSeeding.UnitTests/AbstractSeedTests.cs index 0515ae1..b0a69b9 100644 --- a/Neolution.Extensions.DataSeeding.UnitTests/AbstractSeedTests.cs +++ b/Neolution.Extensions.DataSeeding.UnitTests/AbstractSeedTests.cs @@ -1,4 +1,4 @@ -namespace Neolution.Extensions.DataSeeding.UnitTests +namespace Neolution.Extensions.DataSeeding.UnitTests { using System.Linq; using Microsoft.Extensions.DependencyInjection; @@ -109,4 +109,4 @@ private IServiceCollection CreateServiceCollection() return services; } } -} \ No newline at end of file +} diff --git a/Neolution.Extensions.DataSeeding.UnitTests/BasicTests.cs b/Neolution.Extensions.DataSeeding.UnitTests/BasicTests.cs index bbb9815..6008215 100644 --- a/Neolution.Extensions.DataSeeding.UnitTests/BasicTests.cs +++ b/Neolution.Extensions.DataSeeding.UnitTests/BasicTests.cs @@ -1,4 +1,4 @@ -namespace Neolution.Extensions.DataSeeding.UnitTests +namespace Neolution.Extensions.DataSeeding.UnitTests { using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; diff --git a/Neolution.Extensions.DataSeeding.UnitTests/ScopedServiceLifetimeTests.cs b/Neolution.Extensions.DataSeeding.UnitTests/ScopedServiceLifetimeTests.cs index 992239d..4e0020c 100644 --- a/Neolution.Extensions.DataSeeding.UnitTests/ScopedServiceLifetimeTests.cs +++ b/Neolution.Extensions.DataSeeding.UnitTests/ScopedServiceLifetimeTests.cs @@ -31,11 +31,12 @@ public ScopedServiceLifetimeTests(ITestOutputHelper testOutputHelper) } /// - /// Tests that seeding operations don't leak scoped services outside their scope. + /// Tests that seeding operations work correctly with scoped services without ObjectDisposedException. + /// This verifies the fix for the UserManager issue in console applications. /// /// A task representing the asynchronous test operation. [Fact] - public async Task SeedingOperations_DoNotLeakScopedServices() + public async Task SeedingOperations_WorkCorrectlyWithScopedServices() { // Arrange var services = this.CreateServiceCollection(); @@ -43,12 +44,14 @@ public async Task SeedingOperations_DoNotLeakScopedServices() var serviceProvider = services.BuildServiceProvider(); var seeder = serviceProvider.GetRequiredService(); - // Act + // Act & Assert - This should work without throwing ObjectDisposedException await seeder.SeedAsync(); - // Assert - After seeding, we should not be able to access scoped services from root provider - Should.Throw(() => - serviceProvider.GetRequiredService()); + // Verify we can still resolve scoped services in a new scope (this should work) + using var scope = serviceProvider.CreateScope(); + var scopedService = scope.ServiceProvider.GetRequiredService(); + scopedService.ShouldNotBeNull(); + scopedService.ServiceId.ShouldNotBeEmpty(); } /// diff --git a/Neolution.Extensions.DataSeeding/Seeder.cs b/Neolution.Extensions.DataSeeding/Seeder.cs index 9f123ea..c4a400a 100644 --- a/Neolution.Extensions.DataSeeding/Seeder.cs +++ b/Neolution.Extensions.DataSeeding/Seeder.cs @@ -10,7 +10,7 @@ using Neolution.Extensions.DataSeeding.Internal; /// - [SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "Resolved as a singleton by DI container")] + [SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "Resolved as a transient by DI container")] internal sealed class Seeder : ISeeder { /// diff --git a/Neolution.Extensions.DataSeeding/Seeding.cs b/Neolution.Extensions.DataSeeding/Seeding.cs index b29c5eb..e1bb707 100644 --- a/Neolution.Extensions.DataSeeding/Seeding.cs +++ b/Neolution.Extensions.DataSeeding/Seeding.cs @@ -1,4 +1,4 @@ -namespace Neolution.Extensions.DataSeeding +namespace Neolution.Extensions.DataSeeding { using System; using System.Collections.Generic; @@ -50,7 +50,7 @@ private Seeding() /// Configures the services with the internal dependency injection container and scans the specified assembly for data seeds. /// /// The assembly containing the implementations. - public void Configure(Assembly assembly) + internal void Configure(Assembly assembly) { this.seedsAssembly = assembly; } diff --git a/Neolution.Extensions.DataSeeding/ServiceCollectionExtensions.cs b/Neolution.Extensions.DataSeeding/ServiceCollectionExtensions.cs index 79c9dac..fff1e9d 100644 --- a/Neolution.Extensions.DataSeeding/ServiceCollectionExtensions.cs +++ b/Neolution.Extensions.DataSeeding/ServiceCollectionExtensions.cs @@ -1,4 +1,4 @@ -namespace Neolution.Extensions.DataSeeding +namespace Neolution.Extensions.DataSeeding { using System.Reflection; using Microsoft.Extensions.DependencyInjection; From e84fbc82ef37255cbb51eeb1aefae7f7939f15d5 Mon Sep 17 00:00:00 2001 From: Sandro Ciervo Date: Sun, 27 Jul 2025 03:17:43 +0200 Subject: [PATCH 15/16] Optimize architecture: Remove serviceProvider field duplication Maintain separation of concerns: Seeding for coordination, Seeder for execution --- Neolution.Extensions.DataSeeding/Seeder.cs | 8 +------- Neolution.Extensions.DataSeeding/Seeding.cs | 22 +++++++++++++++++++++ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/Neolution.Extensions.DataSeeding/Seeder.cs b/Neolution.Extensions.DataSeeding/Seeder.cs index c4a400a..b248387 100644 --- a/Neolution.Extensions.DataSeeding/Seeder.cs +++ b/Neolution.Extensions.DataSeeding/Seeder.cs @@ -18,11 +18,6 @@ internal sealed class Seeder : ISeeder /// private readonly ILogger logger; - /// - /// The service provider. - /// - private readonly IServiceProvider serviceProvider; - /// /// Initializes a new instance of the class. /// @@ -31,7 +26,6 @@ internal sealed class Seeder : ISeeder public Seeder(IServiceProvider serviceProvider, ILogger logger) { this.logger = logger; - this.serviceProvider = serviceProvider; Seeding.Instance.UseServiceProvider(serviceProvider); } @@ -68,7 +62,7 @@ public async Task SeedAsync() this.logger.LogDebug("Start seeding..."); // Create a scope to handle scoped dependencies and resolve fresh seeds - using (var scope = this.serviceProvider.CreateScope()) + using (var scope = Seeding.Instance.CreateScope()) { // Resolve fresh instances of seeds within the scope to handle scoped dependencies foreach (var seed in sortedSeeds) diff --git a/Neolution.Extensions.DataSeeding/Seeding.cs b/Neolution.Extensions.DataSeeding/Seeding.cs index e1bb707..ec2ce6e 100644 --- a/Neolution.Extensions.DataSeeding/Seeding.cs +++ b/Neolution.Extensions.DataSeeding/Seeding.cs @@ -24,6 +24,11 @@ internal sealed class Seeding /// private Assembly? seedsAssembly; + /// + /// The service provider. + /// + private IServiceProvider? serviceProvider; + /// /// The seeds /// @@ -66,6 +71,9 @@ internal void UseServiceProvider(IServiceProvider serviceProvider) throw new InvalidOperationException("Cannot find the assembly containing the seeds. Did you call the Configure() method before calling this?"); } + // Store the service provider for creating scopes during execution + this.serviceProvider = serviceProvider; + var logger = serviceProvider.GetRequiredService>(); if (logger.IsEnabled(LogLevel.Trace)) @@ -126,6 +134,20 @@ internal Seed FindSeed() return this.seeds.Single(x => x.GetType() == typeof(T)); } + /// + /// Creates a new service scope for seed execution. + /// + /// A new service scope. + internal IServiceScope CreateScope() + { + if (this.serviceProvider is null) + { + throw new InvalidOperationException("Service provider not configured. Call UseServiceProvider first."); + } + + return this.serviceProvider.CreateScope(); + } + /// /// Finds the seeds that depend on the specified seed type. /// From f320844f5a0d2dc2726bbc688d6f7eaf1bbdd656 Mon Sep 17 00:00:00 2001 From: Sandro Ciervo Date: Sun, 27 Jul 2025 03:24:29 +0200 Subject: [PATCH 16/16] Removes unused attributes --- Neolution.Extensions.DataSeeding/Seeder.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Neolution.Extensions.DataSeeding/Seeder.cs b/Neolution.Extensions.DataSeeding/Seeder.cs index b248387..0e8a145 100644 --- a/Neolution.Extensions.DataSeeding/Seeder.cs +++ b/Neolution.Extensions.DataSeeding/Seeder.cs @@ -2,7 +2,6 @@ { using System; using System.Collections.Generic; - using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -10,7 +9,6 @@ using Neolution.Extensions.DataSeeding.Internal; /// - [SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "Resolved as a transient by DI container")] internal sealed class Seeder : ISeeder { ///