diff --git a/src/Oakton/Oakton.csproj b/src/Oakton/Oakton.csproj index 056b41c0..9bd25113 100644 --- a/src/Oakton/Oakton.csproj +++ b/src/Oakton/Oakton.csproj @@ -22,6 +22,10 @@ + + + + diff --git a/src/Oakton/Resources/ResourceHostExtensions.cs b/src/Oakton/Resources/ResourceHostExtensions.cs index 0a188cb3..8231862b 100644 --- a/src/Oakton/Resources/ResourceHostExtensions.cs +++ b/src/Oakton/Resources/ResourceHostExtensions.cs @@ -1,128 +1,133 @@ -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace Oakton.Resources; - -public enum StartupAction -{ - /// - /// Only check that each resource is set up and functional - /// - SetupOnly, - - /// - /// Check that each resource is set up, functional, and clear off - /// any existing state. This is mainly meant for automated testing scenarios - /// - ResetState -} - -public static class ResourceHostExtensions -{ - /// - /// Add a hosted service that will do setup on all registered stateful resources - /// - /// - /// Configure the startup action. The default is SetupOnly - /// - public static IServiceCollection AddResourceSetupOnStartup(this IServiceCollection services, - StartupAction action = StartupAction.SetupOnly) - { - if (!services.Any(x => - x.ServiceType == typeof(IHostedService) && - x.ImplementationType == typeof(ResourceSetupHostService))) - { - services.Insert(0, - new ServiceDescriptor(typeof(IHostedService), typeof(ResourceSetupHostService), - ServiceLifetime.Singleton)); - services.AddLogging(); - } - - var options = new ResourceSetupOptions { Action = action }; - services.AddSingleton(options); - - return services; - } - - /// - /// Add a hosted service that will do setup on all registered stateful resources - /// - /// - /// Configure the startup action. The default is SetupOnly - /// - public static IHostBuilder UseResourceSetupOnStartup(this IHostBuilder builder, - StartupAction action = StartupAction.SetupOnly) - { - return builder.ConfigureServices(s => s.AddResourceSetupOnStartup(action)); - } - - /// - /// Add a hosted service that will do setup on all registered stateful resources, but only - /// if the environment name is "Development" - /// - /// - /// Configure the startup action. The default is SetupOnly - /// - public static IHostBuilder UseResourceSetupOnStartupInDevelopment(this IHostBuilder builder, - StartupAction action = StartupAction.SetupOnly) - { - return builder.ConfigureServices((context, services) => - { - if (context.HostingEnvironment.IsDevelopment()) - { - services.AddResourceSetupOnStartup(action); - } - }); - } - - /// - /// Executes SetUp(), then ClearState() on all stateful resources. Useful for automated testing scenarios to - /// ensure all resources are in a good, known state - /// - /// - /// - /// Optional filter on resource type name - /// Optional filter on resource name - public static async Task ResetResourceState(this IHost host, CancellationToken cancellation = default, - string resourceType = null, string resourceName = null) - { - var resources = ResourcesCommand.FindResources(host.Services, resourceType, resourceName); - foreach (var resource in resources) - { - await resource.Setup(cancellation); - await resource.ClearState(cancellation); - } - } - - /// - /// Executes SetUp() on all stateful resources. Useful for automated testing scenarios to - /// ensure all resources are in a good, known state - /// - /// - /// - /// Optional filter on resource type name - /// Optional filter on resource name - public static async Task SetupResources(this IHost host, CancellationToken cancellation = default, - string resourceType = null, string resourceName = null) - { - var resources = ResourcesCommand.FindResources(host.Services, resourceType, resourceName); - foreach (var resource in resources) await resource.Setup(cancellation); - } - - /// - /// Executes Teardown() on all stateful resources - /// - /// - /// - /// Optional filter on resource type name - /// Optional filter on resource name - public static async Task TeardownResources(this IHost host, CancellationToken cancellation = default, - string resourceType = null, string resourceName = null) - { - var resources = ResourcesCommand.FindResources(host.Services, resourceType, resourceName); - foreach (var resource in resources) await resource.Teardown(cancellation); - } +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Oakton.Resources; + +public enum StartupAction +{ + /// + /// Only check that each resource is set up and functional + /// + SetupOnly, + + /// + /// Check that each resource is set up, functional, and clear off + /// any existing state. This is mainly meant for automated testing scenarios + /// + ResetState +} + +public static class ResourceHostExtensions +{ + /// + /// Add a hosted service that will do setup on all registered stateful resources + /// + /// + /// Configure the startup action. The default is SetupOnly + /// + public static IServiceCollection AddResourceSetupOnStartup(this IServiceCollection services, + StartupAction action = StartupAction.SetupOnly) + { + if (!services.Any(x => + x.ServiceType == typeof(IHostedService) && +#if NET8_0_OR_GREATER + x.ServiceKey is not null ? + x.KeyedImplementationType == typeof(ResourceSetupHostService) && +#else + x.ImplementationType == typeof(ResourceSetupHostService))) +#endif + { + services.Insert(0, + new ServiceDescriptor(typeof(IHostedService), typeof(ResourceSetupHostService), + ServiceLifetime.Singleton)); + services.AddLogging(); + } + + var options = new ResourceSetupOptions { Action = action }; + services.AddSingleton(options); + + return services; + } + + /// + /// Add a hosted service that will do setup on all registered stateful resources + /// + /// + /// Configure the startup action. The default is SetupOnly + /// + public static IHostBuilder UseResourceSetupOnStartup(this IHostBuilder builder, + StartupAction action = StartupAction.SetupOnly) + { + return builder.ConfigureServices(s => s.AddResourceSetupOnStartup(action)); + } + + /// + /// Add a hosted service that will do setup on all registered stateful resources, but only + /// if the environment name is "Development" + /// + /// + /// Configure the startup action. The default is SetupOnly + /// + public static IHostBuilder UseResourceSetupOnStartupInDevelopment(this IHostBuilder builder, + StartupAction action = StartupAction.SetupOnly) + { + return builder.ConfigureServices((context, services) => + { + if (context.HostingEnvironment.IsDevelopment()) + { + services.AddResourceSetupOnStartup(action); + } + }); + } + + /// + /// Executes SetUp(), then ClearState() on all stateful resources. Useful for automated testing scenarios to + /// ensure all resources are in a good, known state + /// + /// + /// + /// Optional filter on resource type name + /// Optional filter on resource name + public static async Task ResetResourceState(this IHost host, CancellationToken cancellation = default, + string resourceType = null, string resourceName = null) + { + var resources = ResourcesCommand.FindResources(host.Services, resourceType, resourceName); + foreach (var resource in resources) + { + await resource.Setup(cancellation); + await resource.ClearState(cancellation); + } + } + + /// + /// Executes SetUp() on all stateful resources. Useful for automated testing scenarios to + /// ensure all resources are in a good, known state + /// + /// + /// + /// Optional filter on resource type name + /// Optional filter on resource name + public static async Task SetupResources(this IHost host, CancellationToken cancellation = default, + string resourceType = null, string resourceName = null) + { + var resources = ResourcesCommand.FindResources(host.Services, resourceType, resourceName); + foreach (var resource in resources) await resource.Setup(cancellation); + } + + /// + /// Executes Teardown() on all stateful resources + /// + /// + /// + /// Optional filter on resource type name + /// Optional filter on resource name + public static async Task TeardownResources(this IHost host, CancellationToken cancellation = default, + string resourceType = null, string resourceName = null) + { + var resources = ResourcesCommand.FindResources(host.Services, resourceType, resourceName); + foreach (var resource in resources) await resource.Teardown(cancellation); + } } \ No newline at end of file diff --git a/src/Tests/Resources/ResourceHostExtensionsTests.cs b/src/Tests/Resources/ResourceHostExtensionsTests.cs index 94a6ad9a..41a97197 100644 --- a/src/Tests/Resources/ResourceHostExtensionsTests.cs +++ b/src/Tests/Resources/ResourceHostExtensionsTests.cs @@ -1,310 +1,338 @@ -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Lamar; -using Lamar.Microsoft.DependencyInjection; -using Microsoft.Extensions.Hosting; -using NSubstitute; -using NSubstitute.ReceivedExtensions; -using Oakton.Resources; -using Shouldly; -using Xunit; - -namespace Tests.Resources -{ - public class ResourceHostExtensionsTests : ResourceCommandContext - { - public static async Task sample1() - { - #region sample_using_AddResourceSetupOnStartup - - using var host = await Host.CreateDefaultBuilder() - .ConfigureServices(services => - { - // More service registrations like this is a real app! - - services.AddResourceSetupOnStartup(); - }).StartAsync(); - - #endregion - } - - public static async Task sample2() - { - #region sample_using_AddResourceSetupOnStartup2 - - using var host = await Host.CreateDefaultBuilder() - .ConfigureServices(services => - { - // More service registrations like this is a real app! - }) - .UseResourceSetupOnStartup() - .StartAsync(); - - #endregion - } - - public static async Task sample3() - { - #region sample_using_AddResourceSetupOnStartup3 - - using var host = await Host.CreateDefaultBuilder() - .ConfigureServices(services => - { - // More service registrations like this is a real app! - }) - .UseResourceSetupOnStartupInDevelopment() - .StartAsync(); - - #endregion - } - - #region sample_programmatically_control_resources - - public static async Task usages_for_testing(IHost host) - { - // Programmatically call Setup() on all resources - await host.SetupResources(); - - // Maybe between integration tests, clear any - // persisted state. For example, I've used this to - // purge Rabbit MQ queues between tests - await host.ResetResourceState(); - - // Tear it all down! - await host.TeardownResources(); - } - - #endregion - - [Fact] - public void add_resource_startup() - { - using var container = Container.For(services => - { - services.AddResourceSetupOnStartup(); - - // Only does it once! - services.AddResourceSetupOnStartup(); - services.AddResourceSetupOnStartup(); - services.AddResourceSetupOnStartup(); - services.AddResourceSetupOnStartup(); - }); - - container.Model.For() - .Instances.Single().ImplementationType.ShouldBe(typeof(ResourceSetupHostService)); - - container.GetInstance() - .ShouldBeOfType(); - } - - [Fact] - public void use_resource_setup() - { - using var host = Host.CreateDefaultBuilder() - .UseLamar() - .UseResourceSetupOnStartup() - .Build(); - - var container = (IContainer)host.Services; - - container.Model.For() - .Instances.Single().ImplementationType.ShouldBe(typeof(ResourceSetupHostService)); - - container.GetInstance() - .ShouldBeOfType(); - } - - [Fact] - public void use_conditional_resource_setup_in_development() - { - using var host = Host.CreateDefaultBuilder() - .UseLamar() - .UseResourceSetupOnStartupInDevelopment() - .UseEnvironment("Development") - .Build(); - - var container = (IContainer)host.Services; - - container.Model.For() - .Instances.Single().ImplementationType.ShouldBe(typeof(ResourceSetupHostService)); - - container.GetInstance() - .ShouldBeOfType(); - } - - [Fact] - public void use_conditional_resource_setup_only_in_development_does_nothing_in_prod() - { - using var host = Host.CreateDefaultBuilder() - .UseLamar() - .UseResourceSetupOnStartupInDevelopment() - .UseEnvironment("Production") - .Build(); - - var container = (IContainer)host.Services; - - container.Model.For() - .Instances.Any().ShouldBeFalse(); - } - - [Fact] - public async Task runs_all_resources() - { - var blue = AddResource("blue", "color"); - var red = AddResource("red", "color"); - - AddSource(col => - { - col.Add("purple", "color"); - col.Add("orange", "color"); - }); - - AddSource(col => - { - col.Add("green", "color"); - col.Add("white", "color"); - }); - - using var host = await Host.CreateDefaultBuilder() - .UseResourceSetupOnStartup() - .ConfigureServices(services => - { - CopyResources(services); - services.AddResourceSetupOnStartup(); - }) - .StartAsync(); - - foreach (var resource in AllResources) - { - await resource.Received().Setup(Arg.Any()); - await resource.DidNotReceive().ClearState(Arg.Any()); - } - - } - - - [Fact] - public async Task runs_all_resources_and_resets() - { - var blue = AddResource("blue", "color"); - var red = AddResource("red", "color"); - - AddSource(col => - { - col.Add("purple", "color"); - col.Add("orange", "color"); - }); - - AddSource(col => - { - col.Add("green", "color"); - col.Add("white", "color"); - }); - - using var host = await Host.CreateDefaultBuilder() - .UseResourceSetupOnStartup() - .ConfigureServices(services => - { - CopyResources(services); - services.AddResourceSetupOnStartup(StartupAction.ResetState); - }) - .StartAsync(); - - foreach (var resource in AllResources) - { - await resource.Received().Setup(Arg.Any()); - await resource.Received().ClearState(Arg.Any()); - } - - } - - [Fact] - public async Task setup_all() - { - var blue = AddResource("blue", "color"); - var red = AddResource("red", "color"); - - AddSource(col => - { - col.Add("purple", "color"); - col.Add("orange", "color"); - }); - - AddSource(col => - { - col.Add("green", "color"); - col.Add("white", "color"); - }); - - using var host = await buildHost(); - await host.SetupResources(); - - foreach (var resource in AllResources) - { - await resource.Received().Setup(Arg.Any()); - } - - - } - - [Fact] - public async Task reset_all() - { - var blue = AddResource("blue", "color"); - var red = AddResource("red", "color"); - - AddSource(col => - { - col.Add("purple", "color"); - col.Add("orange", "color"); - }); - - AddSource(col => - { - col.Add("green", "color"); - col.Add("white", "color"); - }); - - using var host = await buildHost(); - await host.ResetResourceState(); - - foreach (var resource in AllResources) - { - await resource.Received().Setup(Arg.Any()); - await resource.Received().ClearState(Arg.Any()); - } - - - } - - [Fact] - public async Task teardown_all() - { - var blue = AddResource("blue", "color"); - var red = AddResource("red", "color"); - - AddSource(col => - { - col.Add("purple", "color"); - col.Add("orange", "color"); - }); - - AddSource(col => - { - col.Add("green", "color"); - col.Add("white", "color"); - }); - - using var host = await buildHost(); - await host.TeardownResources(); - - foreach (var resource in AllResources) - { - await resource.Received().Teardown(Arg.Any()); - } - - - } - } +using Lamar; +using Lamar.Microsoft.DependencyInjection; +#if NET8_0_OR_GREATER +using Microsoft.Extensions.DependencyInjection; +#endif +using Microsoft.Extensions.Hosting; +using NSubstitute; +using NSubstitute.ReceivedExtensions; +using Oakton.Resources; +using Shouldly; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Tests.Resources +{ + public class ResourceHostExtensionsTests : ResourceCommandContext + { + public static async Task sample1() + { + #region sample_using_AddResourceSetupOnStartup + + using var host = await Host.CreateDefaultBuilder() + .ConfigureServices(services => + { + // More service registrations like this is a real app! + + services.AddResourceSetupOnStartup(); + }).StartAsync(); + + #endregion + } + + public static async Task sample2() + { + #region sample_using_AddResourceSetupOnStartup2 + + using var host = await Host.CreateDefaultBuilder() + .ConfigureServices(services => + { + // More service registrations like this is a real app! + }) + .UseResourceSetupOnStartup() + .StartAsync(); + + #endregion + } + + public static async Task sample3() + { + #region sample_using_AddResourceSetupOnStartup3 + + using var host = await Host.CreateDefaultBuilder() + .ConfigureServices(services => + { + // More service registrations like this is a real app! + }) + .UseResourceSetupOnStartupInDevelopment() + .StartAsync(); + + #endregion + } + + #region sample_programmatically_control_resources + + public static async Task usages_for_testing(IHost host) + { + // Programmatically call Setup() on all resources + await host.SetupResources(); + + // Maybe between integration tests, clear any + // persisted state. For example, I've used this to + // purge Rabbit MQ queues between tests + await host.ResetResourceState(); + + // Tear it all down! + await host.TeardownResources(); + } + + #endregion + + [Fact] + public void add_resource_startup() + { + using var container = Container.For(services => + { + services.AddResourceSetupOnStartup(); + + // Only does it once! + services.AddResourceSetupOnStartup(); + services.AddResourceSetupOnStartup(); + services.AddResourceSetupOnStartup(); + services.AddResourceSetupOnStartup(); + }); + + container.Model.For() + .Instances.Single().ImplementationType.ShouldBe(typeof(ResourceSetupHostService)); + + container.GetInstance() + .ShouldBeOfType(); + } + +#if NET8_0_OR_GREATER + [Fact] + public void add_resource_startup_can_handle_keyed_services() + { + using var container = Container.For(services => + { + + // Add a keyed service. + services.AddKeyedSingleton("test", this); + + // This should not throw. + services.AddResourceSetupOnStartup(); + + // Verify the number of services added by AddTokenAcquisition (ignoring the service we added here). + services.Count(t => t.ServiceType != typeof(ResourceHostExtensionsTests)).ShouldBe(10); + }); + + container.Model.For() + .Instances.Single().ImplementationType.ShouldBe(typeof(ResourceSetupHostService)); + + container.GetInstance() + .ShouldBeOfType(); + } +#endif + + [Fact] + public void use_resource_setup() + { + using var host = Host.CreateDefaultBuilder() + .UseLamar() + .UseResourceSetupOnStartup() + .Build(); + + var container = (IContainer)host.Services; + + container.Model.For() + .Instances.Single().ImplementationType.ShouldBe(typeof(ResourceSetupHostService)); + + container.GetInstance() + .ShouldBeOfType(); + } + + [Fact] + public void use_conditional_resource_setup_in_development() + { + using var host = Host.CreateDefaultBuilder() + .UseLamar() + .UseResourceSetupOnStartupInDevelopment() + .UseEnvironment("Development") + .Build(); + + var container = (IContainer)host.Services; + + container.Model.For() + .Instances.Single().ImplementationType.ShouldBe(typeof(ResourceSetupHostService)); + + container.GetInstance() + .ShouldBeOfType(); + } + + [Fact] + public void use_conditional_resource_setup_only_in_development_does_nothing_in_prod() + { + using var host = Host.CreateDefaultBuilder() + .UseLamar() + .UseResourceSetupOnStartupInDevelopment() + .UseEnvironment("Production") + .Build(); + + var container = (IContainer)host.Services; + + container.Model.For() + .Instances.Any().ShouldBeFalse(); + } + + [Fact] + public async Task runs_all_resources() + { + var blue = AddResource("blue", "color"); + var red = AddResource("red", "color"); + + AddSource(col => + { + col.Add("purple", "color"); + col.Add("orange", "color"); + }); + + AddSource(col => + { + col.Add("green", "color"); + col.Add("white", "color"); + }); + + using var host = await Host.CreateDefaultBuilder() + .UseResourceSetupOnStartup() + .ConfigureServices(services => + { + CopyResources(services); + services.AddResourceSetupOnStartup(); + }) + .StartAsync(); + + foreach (var resource in AllResources) + { + await resource.Received().Setup(Arg.Any()); + await resource.DidNotReceive().ClearState(Arg.Any()); + } + + } + + + [Fact] + public async Task runs_all_resources_and_resets() + { + var blue = AddResource("blue", "color"); + var red = AddResource("red", "color"); + + AddSource(col => + { + col.Add("purple", "color"); + col.Add("orange", "color"); + }); + + AddSource(col => + { + col.Add("green", "color"); + col.Add("white", "color"); + }); + + using var host = await Host.CreateDefaultBuilder() + .UseResourceSetupOnStartup() + .ConfigureServices(services => + { + CopyResources(services); + services.AddResourceSetupOnStartup(StartupAction.ResetState); + }) + .StartAsync(); + + foreach (var resource in AllResources) + { + await resource.Received().Setup(Arg.Any()); + await resource.Received().ClearState(Arg.Any()); + } + + } + + [Fact] + public async Task setup_all() + { + var blue = AddResource("blue", "color"); + var red = AddResource("red", "color"); + + AddSource(col => + { + col.Add("purple", "color"); + col.Add("orange", "color"); + }); + + AddSource(col => + { + col.Add("green", "color"); + col.Add("white", "color"); + }); + + using var host = await buildHost(); + await host.SetupResources(); + + foreach (var resource in AllResources) + { + await resource.Received().Setup(Arg.Any()); + } + + + } + + [Fact] + public async Task reset_all() + { + var blue = AddResource("blue", "color"); + var red = AddResource("red", "color"); + + AddSource(col => + { + col.Add("purple", "color"); + col.Add("orange", "color"); + }); + + AddSource(col => + { + col.Add("green", "color"); + col.Add("white", "color"); + }); + + using var host = await buildHost(); + await host.ResetResourceState(); + + foreach (var resource in AllResources) + { + await resource.Received().Setup(Arg.Any()); + await resource.Received().ClearState(Arg.Any()); + } + + + } + + [Fact] + public async Task teardown_all() + { + var blue = AddResource("blue", "color"); + var red = AddResource("red", "color"); + + AddSource(col => + { + col.Add("purple", "color"); + col.Add("orange", "color"); + }); + + AddSource(col => + { + col.Add("green", "color"); + col.Add("white", "color"); + }); + + using var host = await buildHost(); + await host.TeardownResources(); + + foreach (var resource in AllResources) + { + await resource.Received().Teardown(Arg.Any()); + } + + + } + } } \ No newline at end of file diff --git a/src/Tests/Tests.csproj b/src/Tests/Tests.csproj index a5243ce6..e2eff115 100644 --- a/src/Tests/Tests.csproj +++ b/src/Tests/Tests.csproj @@ -1,4 +1,4 @@ - + net6.0;net7.0;net8.0 @@ -22,6 +22,10 @@ + + + +