diff --git a/Neolution.Extensions.DataSeeding.UnitTests/AbstractSeedTests.cs b/Neolution.Extensions.DataSeeding.UnitTests/AbstractSeedTests.cs index 05c2d20..b0a69b9 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; @@ -96,6 +97,15 @@ 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(); + + // 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 eb4d364..6008215 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; @@ -54,6 +55,14 @@ 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(); + + // Register the scoped service with dependency to test UserManager-like scenarios + services.AddScoped(); return services; } } diff --git a/Neolution.Extensions.DataSeeding.UnitTests/Fakes/MultiTenantSeeds/ApplicationSettingsSeed.cs b/Neolution.Extensions.DataSeeding.UnitTests/Fakes/MultiTenantSeeds/ApplicationSettingsSeed.cs index ee591ba..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 @@ -12,19 +13,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.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/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/FakeScopedServiceWithDependency.cs b/Neolution.Extensions.DataSeeding.UnitTests/Fakes/Services/FakeScopedServiceWithDependency.cs new file mode 100644 index 0000000..cc6c66c --- /dev/null +++ b/Neolution.Extensions.DataSeeding.UnitTests/Fakes/Services/FakeScopedServiceWithDependency.cs @@ -0,0 +1,38 @@ +namespace Neolution.Extensions.DataSeeding.UnitTests.Fakes.Services +{ + using System; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + + /// + /// A scoped service that depends on other scoped services, similar to UserManager. + /// + public class FakeScopedServiceWithDependency : IFakeScopedServiceWithDependency + { + /// + /// The service provider. + /// + 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/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/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/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; } + } +} 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.UnitTests/ScopedServiceLifetimeTests.cs b/Neolution.Extensions.DataSeeding.UnitTests/ScopedServiceLifetimeTests.cs new file mode 100644 index 0000000..4e0020c --- /dev/null +++ b/Neolution.Extensions.DataSeeding.UnitTests/ScopedServiceLifetimeTests.cs @@ -0,0 +1,114 @@ +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 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_WorkCorrectlyWithScopedServices() + { + // Arrange + var services = this.CreateServiceCollection(); + services.AddDataSeeding(typeof(ScopedServiceLifetimeTests).Assembly); + var serviceProvider = services.BuildServiceProvider(); + var seeder = serviceProvider.GetRequiredService(); + + // Act & Assert - This should work without throwing ObjectDisposedException + await seeder.SeedAsync(); + + // 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(); + } + + /// + /// 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); + } + } +} diff --git a/Neolution.Extensions.DataSeeding/Seeder.cs b/Neolution.Extensions.DataSeeding/Seeder.cs index 4bb8d1f..0e8a145 100644 --- a/Neolution.Extensions.DataSeeding/Seeder.cs +++ b/Neolution.Extensions.DataSeeding/Seeder.cs @@ -2,14 +2,13 @@ { using System; 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; /// - [SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "Resolved as a singleton by DI container")] internal sealed class Seeder : ISeeder { /// @@ -59,9 +58,19 @@ public async Task SeedAsync() } this.logger.LogDebug("Start seeding..."); - foreach (var seed in sortedSeeds) + + // Create a scope to handle scoped dependencies and resolve fresh seeds + using (var scope = Seeding.Instance.CreateScope()) { - await seed.SeedAsync().ConfigureAwait(false); + // Resolve fresh instances of seeds within the scope to handle scoped dependencies + foreach (var seed in sortedSeeds) + { + // 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); + } } this.logger.LogDebug("All seeds have been seeded!"); diff --git a/Neolution.Extensions.DataSeeding/Seeding.cs b/Neolution.Extensions.DataSeeding/Seeding.cs index 7c39ebe..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 /// @@ -50,7 +55,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; } @@ -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)) @@ -74,9 +82,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); @@ -122,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. /// 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()); }