From 432c417ee5f7d1598de837495e4de97853e8bb4d Mon Sep 17 00:00:00 2001 From: Martin Costello Date: Sat, 18 May 2024 07:49:19 +0100 Subject: [PATCH] Do not run IHostedService implementations (#2880) - Remove `IHostedService` implementations so the CLI doesn't run hosted services (and then hang/fail). - Refactor CLI tests to reduce duplication. - Use newer C# syntax as suggested by Visual Studio. Co-authored-by: Sjoerd van der Meer <2460430+desjoerd@users.noreply.github.com> --- Swashbuckle.AspNetCore.sln | 7 + .../CommandRunner.cs | 8 +- .../HostFactoryResolver.cs | 146 +++++++++--------- .../HostingApplication.cs | 27 +++- .../CommandRunnerTests.cs | 38 ++--- .../Swashbuckle.AspNetCore.Cli.Test.csproj | 1 + .../ToolTests.cs | 133 +++++++++++----- .../MinimalAppWithHostedServices.csproj | 18 +++ .../MinimalAppWithHostedServices/Program.cs | 34 ++++ .../Properties/launchSettings.json | 12 ++ 10 files changed, 279 insertions(+), 145 deletions(-) create mode 100644 test/WebSites/MinimalAppWithHostedServices/MinimalAppWithHostedServices.csproj create mode 100644 test/WebSites/MinimalAppWithHostedServices/Program.cs create mode 100644 test/WebSites/MinimalAppWithHostedServices/Properties/launchSettings.json diff --git a/Swashbuckle.AspNetCore.sln b/Swashbuckle.AspNetCore.sln index 59cedef3ae..7fcf8c3e97 100644 --- a/Swashbuckle.AspNetCore.sln +++ b/Swashbuckle.AspNetCore.sln @@ -113,6 +113,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApi", "test\WebSites\Web EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApi.Aot", "test\WebSites\WebApi.Aot\WebApi.Aot.csproj", "{07BB09CF-6C6F-4D00-A459-93586345C921}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MinimalAppWithHostedServices", "test\WebSites\MinimalAppWithHostedServices\MinimalAppWithHostedServices.csproj", "{D06A88E8-6F42-4F40-943A-E266C0AE6EC9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -259,6 +261,10 @@ Global {07BB09CF-6C6F-4D00-A459-93586345C921}.Debug|Any CPU.Build.0 = Debug|Any CPU {07BB09CF-6C6F-4D00-A459-93586345C921}.Release|Any CPU.ActiveCfg = Release|Any CPU {07BB09CF-6C6F-4D00-A459-93586345C921}.Release|Any CPU.Build.0 = Release|Any CPU + {D06A88E8-6F42-4F40-943A-E266C0AE6EC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D06A88E8-6F42-4F40-943A-E266C0AE6EC9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D06A88E8-6F42-4F40-943A-E266C0AE6EC9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D06A88E8-6F42-4F40-943A-E266C0AE6EC9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -302,6 +308,7 @@ Global {B6037A37-4A4F-438D-B18A-0C9D1408EAB2} = {DB3F57FC-1472-4F03-B551-43394DA3C5EB} {DE1D77F8-3916-4DEE-A57D-6DDC357F64C6} = {DB3F57FC-1472-4F03-B551-43394DA3C5EB} {07BB09CF-6C6F-4D00-A459-93586345C921} = {DB3F57FC-1472-4F03-B551-43394DA3C5EB} + {D06A88E8-6F42-4F40-943A-E266C0AE6EC9} = {DB3F57FC-1472-4F03-B551-43394DA3C5EB} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {36FC6A67-247D-4149-8EDD-79FFD1A75F51} diff --git a/src/Swashbuckle.AspNetCore.Cli/CommandRunner.cs b/src/Swashbuckle.AspNetCore.Cli/CommandRunner.cs index 3e9384f9a5..3bccca13b4 100644 --- a/src/Swashbuckle.AspNetCore.Cli/CommandRunner.cs +++ b/src/Swashbuckle.AspNetCore.Cli/CommandRunner.cs @@ -17,10 +17,10 @@ public CommandRunner(string commandName, string commandDescription, TextWriter o { CommandName = commandName; CommandDescription = commandDescription; - _argumentDescriptors = new Dictionary(); - _optionDescriptors = new Dictionary(); - _runFunc = (namedArgs) => { return 1; }; // noop - _subRunners = new List(); + _argumentDescriptors = []; + _optionDescriptors = []; + _runFunc = (_) => 1; // no-op + _subRunners = []; _output = output; } diff --git a/src/Swashbuckle.AspNetCore.Cli/HostFactoryResolver.cs b/src/Swashbuckle.AspNetCore.Cli/HostFactoryResolver.cs index acc5ff59ad..9a4ec584fb 100644 --- a/src/Swashbuckle.AspNetCore.Cli/HostFactoryResolver.cs +++ b/src/Swashbuckle.AspNetCore.Cli/HostFactoryResolver.cs @@ -89,7 +89,7 @@ private static Func ResolveFactory(Assembly assembly, string nam return null; } - return args => (T)factory.Invoke(null, new object[] { args }); + return args => (T)factory.Invoke(null, [args]); } // TReturn Factory(string[] args); @@ -153,7 +153,7 @@ public static Func ResolveServiceProviderFactory(Ass private static object Build(object builder) { var buildMethod = builder.GetType().GetMethod("Build"); - return buildMethod?.Invoke(builder, Array.Empty()); + return buildMethod?.Invoke(builder, []); } private static IServiceProvider GetServiceProvider(object host) @@ -174,13 +174,19 @@ private sealed class HostingListener : IObserver, IObserver< private readonly TimeSpan _waitTimeout; private readonly bool _stopApplication; - private readonly TaskCompletionSource _hostTcs = new TaskCompletionSource(); + private readonly TaskCompletionSource _hostTcs = new(); private IDisposable _disposable; - private Action _configure; - private Action _entrypointCompleted; - private static readonly AsyncLocal _currentListener = new AsyncLocal(); - - public HostingListener(string[] args, MethodInfo entryPoint, TimeSpan waitTimeout, bool stopApplication, Action configure, Action entrypointCompleted) + private readonly Action _configure; + private readonly Action _entrypointCompleted; + private static readonly AsyncLocal _currentListener = new(); + + public HostingListener( + string[] args, + MethodInfo entryPoint, + TimeSpan waitTimeout, + bool stopApplication, + Action configure, + Action entrypointCompleted) { _args = args; _entryPoint = entryPoint; @@ -192,84 +198,82 @@ public HostingListener(string[] args, MethodInfo entryPoint, TimeSpan waitTimeou public object CreateHost() { - using (var subscription = DiagnosticListener.AllListeners.Subscribe(this)) + using var subscription = DiagnosticListener.AllListeners.Subscribe(this); + + // Kick off the entry point on a new thread so we don't block the current one + // in case we need to timeout the execution + var thread = new Thread(() => { + Exception exception = null; - // Kick off the entry point on a new thread so we don't block the current one - // in case we need to timeout the execution - var thread = new Thread(() => + try { - Exception exception = null; + // Set the async local to the instance of the HostingListener so we can filter events that + // aren't scoped to this execution of the entry point. + _currentListener.Value = this; - try - { - // Set the async local to the instance of the HostingListener so we can filter events that - // aren't scoped to this execution of the entry point. - _currentListener.Value = this; - - var parameters = _entryPoint.GetParameters(); - if (parameters.Length == 0) - { - _entryPoint.Invoke(null, Array.Empty()); - } - else - { - _entryPoint.Invoke(null, new object[] { _args }); - } - - // Try to set an exception if the entry point returns gracefully, this will force - // build to throw - _hostTcs.TrySetException(new InvalidOperationException("Unable to build IHost")); - } - catch (TargetInvocationException tie) when (tie.InnerException is StopTheHostException) + var parameters = _entryPoint.GetParameters(); + if (parameters.Length == 0) { - // The host was stopped by our own logic + _entryPoint.Invoke(null, []); } - catch (TargetInvocationException tie) + else { - exception = tie.InnerException ?? tie; - - // Another exception happened, propagate that to the caller - _hostTcs.TrySetException(exception); + _entryPoint.Invoke(null, [_args]); } - catch (Exception ex) - { - exception = ex; - // Another exception happened, propagate that to the caller - _hostTcs.TrySetException(ex); - } - finally - { - // Signal that the entry point is completed - _entrypointCompleted?.Invoke(exception); - } - }) + // Try to set an exception if the entry point returns gracefully, this will force + // build to throw + _hostTcs.TrySetException(new InvalidOperationException("Unable to build IHost")); + } + catch (TargetInvocationException tie) when (tie.InnerException is StopTheHostException) { - // Make sure this doesn't hang the process - IsBackground = true - }; - - // Start the thread - thread.Start(); + // The host was stopped by our own logic + } + catch (TargetInvocationException tie) + { + exception = tie.InnerException ?? tie; - try + // Another exception happened, propagate that to the caller + _hostTcs.TrySetException(exception); + } + catch (Exception ex) { - // Wait before throwing an exception - if (!_hostTcs.Task.Wait(_waitTimeout)) - { - throw new InvalidOperationException("Unable to build IHost"); - } + exception = ex; + + // Another exception happened, propagate that to the caller + _hostTcs.TrySetException(ex); } - catch (AggregateException) when (_hostTcs.Task.IsCompleted) + finally { - // Lets this propagate out of the call to GetAwaiter().GetResult() + // Signal that the entry point is completed + _entrypointCompleted?.Invoke(exception); } + }) + { + // Make sure this doesn't hang the process + IsBackground = true + }; - Debug.Assert(_hostTcs.Task.IsCompleted); + // Start the thread + thread.Start(); - return _hostTcs.Task.GetAwaiter().GetResult(); + try + { + // Wait before throwing an exception + if (!_hostTcs.Task.Wait(_waitTimeout)) + { + throw new InvalidOperationException("Unable to build IHost"); + } } + catch (AggregateException) when (_hostTcs.Task.IsCompleted) + { + // Lets this propagate out of the call to GetAwaiter().GetResult() + } + + Debug.Assert(_hostTcs.Task.IsCompleted); + + return _hostTcs.Task.GetAwaiter().GetResult(); } public void OnCompleted() @@ -279,7 +283,6 @@ public void OnCompleted() public void OnError(Exception error) { - } public void OnNext(DiagnosticListener value) @@ -321,10 +324,7 @@ public void OnNext(KeyValuePair value) } } - private sealed class StopTheHostException : Exception - { - - } + private sealed class StopTheHostException : Exception; } } } diff --git a/src/Swashbuckle.AspNetCore.Cli/HostingApplication.cs b/src/Swashbuckle.AspNetCore.Cli/HostingApplication.cs index f17c6c1419..3c7685c088 100644 --- a/src/Swashbuckle.AspNetCore.Cli/HostingApplication.cs +++ b/src/Swashbuckle.AspNetCore.Cli/HostingApplication.cs @@ -4,7 +4,9 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Http.Features; +#if NETCOREAPP3_0_OR_GREATER using Microsoft.Extensions.DependencyInjection; +#endif using Microsoft.Extensions.Hosting; namespace Swashbuckle.AspNetCore.Cli @@ -31,6 +33,19 @@ void ConfigureHostBuilder(object hostBuilder) { services.AddSingleton(); services.AddSingleton(); + + for (var i = services.Count - 1; i >= 0; i--) + { + // exclude all implementations of IHostedService + // except Microsoft.AspNetCore.Hosting.GenericWebHostService because that one will build/configure + // the WebApplication/Middleware pipeline in the case of the GenericWebHostBuilder. + var registration = services[i]; + if (registration.ServiceType == typeof(IHostedService) + && registration.ImplementationType is not { FullName: "Microsoft.AspNetCore.Hosting.GenericWebHostService" }) + { + services.RemoveAt(i); + } + } }); } @@ -69,18 +84,17 @@ void OnEntryPointExit(Exception exception) // We set the application name in the hosting environment to the startup assembly // to avoid falling back to the entry assembly (dotnet-swagger) when configuring our // application. - var services = ((IHost)factory(new[] { $"--{HostDefaults.ApplicationKey}={assemblyName}" })).Services; + var services = ((IHost)factory([$"--{HostDefaults.ApplicationKey}={assemblyName}"])).Services; // Wait for the application to start so that we know it's fully configured. This is important because // we need the middleware pipeline to be configured before we access the ISwaggerProvider in // in the IServiceProvider var applicationLifetime = services.GetRequiredService(); - using (var registration = applicationLifetime.ApplicationStarted.Register(() => waitForStartTcs.TrySetResult(null))) - { - waitForStartTcs.Task.Wait(); - return services; - } + using var registration = applicationLifetime.ApplicationStarted.Register(() => waitForStartTcs.TrySetResult(null)); + waitForStartTcs.Task.Wait(); + + return services; } catch (InvalidOperationException) { @@ -103,7 +117,6 @@ private class NoopServer : IServer public void Dispose() { } public Task StartAsync(IHttpApplication application, CancellationToken cancellationToken) => Task.CompletedTask; public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; - } } } diff --git a/test/Swashbuckle.AspNetCore.Cli.Test/CommandRunnerTests.cs b/test/Swashbuckle.AspNetCore.Cli.Test/CommandRunnerTests.cs index 65d0a8c311..dd01930e22 100644 --- a/test/Swashbuckle.AspNetCore.Cli.Test/CommandRunnerTests.cs +++ b/test/Swashbuckle.AspNetCore.Cli.Test/CommandRunnerTests.cs @@ -4,10 +4,10 @@ namespace Swashbuckle.AspNetCore.Cli.Test { - public class CommandRunnerTests + public static class CommandRunnerTests { [Fact] - public void Run_ParsesArgumentsAndExecutesCommands_AccordingToConfiguredMetadata() + public static void Run_ParsesArgumentsAndExecutesCommands_AccordingToConfiguredMetadata() { var receivedValues = new List(); var subject = new CommandRunner("test", "a test", new StringWriter()); @@ -36,23 +36,23 @@ public void Run_ParsesArgumentsAndExecutesCommands_AccordingToConfiguredMetadata }); }); - var cmd1ExitCode = subject.Run(new[] { "cmd1", "--opt1", "foo", "--opt2", "bar" }); - var cmd2ExitCode = subject.Run(new[] { "cmd2", "--opt1", "blah", "--opt2", "dblah" }); + var cmd1ExitCode = subject.Run(["cmd1", "--opt1", "foo", "--opt2", "bar"]); + var cmd2ExitCode = subject.Run(["cmd2", "--opt1", "blah", "--opt2", "dblah"]); Assert.Equal(2, cmd1ExitCode); Assert.Equal(3, cmd2ExitCode); - Assert.Equal(new[] { "foo", null, "bar", "blah", null, "dblah" }, receivedValues.ToArray()); + Assert.Equal(["foo", null, "bar", "blah", null, "dblah"], [.. receivedValues]); } [Fact] - public void Run_PrintsAvailableCommands_WhenUnexpectedCommandIsProvided() + public static void Run_PrintsAvailableCommands_WhenUnexpectedCommandIsProvided() { var output = new StringWriter(); var subject = new CommandRunner("test", "a test", output); subject.SubCommand("cmd", "does something", c => { }); - var exitCode = subject.Run(new[] { "foo" }); + var exitCode = subject.Run(["foo"]); Assert.StartsWith("a test", output.ToString()); Assert.Contains("Commands:", output.ToString()); @@ -60,14 +60,14 @@ public void Run_PrintsAvailableCommands_WhenUnexpectedCommandIsProvided() } [Fact] - public void Run_PrintsAvailableCommands_WhenHelpOptionIsProvided() + public static void Run_PrintsAvailableCommands_WhenHelpOptionIsProvided() { var output = new StringWriter(); var subject = new CommandRunner("test", "a test", output); subject.SubCommand("cmd", "does something", c => { }); - var exitCode = subject.Run(new[] { "--help" }); + var exitCode = subject.Run(["--help"]); Assert.StartsWith("a test", output.ToString()); Assert.Contains("Commands:", output.ToString()); @@ -75,15 +75,15 @@ public void Run_PrintsAvailableCommands_WhenHelpOptionIsProvided() } [Theory] - [InlineData(new[] { "--opt1" }, new string[] { }, new[] { "cmd", "--opt2", "foo" }, true)] - [InlineData(new[] { "--opt1" }, new string[] { }, new[] { "cmd", "--opt1" }, true)] - [InlineData(new[] { "--opt1" }, new string[] { }, new[] { "cmd", "--opt1", "--opt2" }, true)] - [InlineData(new[] { "--opt1" }, new string[] { }, new[] { "cmd", "--opt1", "foo" }, false)] - [InlineData(new string[] { }, new[] { "arg1" }, new[] { "cmd" }, true)] - [InlineData(new string[] { }, new[] { "arg1" }, new[] { "cmd", "--opt1" }, true)] - [InlineData(new string[] {}, new[] { "arg1" }, new[] { "cmd", "foo", "bar" }, true)] - [InlineData(new string[] {}, new[] { "arg1" }, new[] { "cmd", "foo" }, false)] - public void Run_PrintsCommandUsage_WhenUnexpectedArgumentsAreProvided( + [InlineData(new[] { "--opt1" }, new string[0], new[] { "cmd", "--opt2", "foo" }, true)] + [InlineData(new[] { "--opt1" }, new string[0], new[] { "cmd", "--opt1" }, true)] + [InlineData(new[] { "--opt1" }, new string[0], new[] { "cmd", "--opt1", "--opt2" }, true)] + [InlineData(new[] { "--opt1" }, new string[0], new[] { "cmd", "--opt1", "foo" }, false)] + [InlineData(new string[0], new[] { "arg1" }, new[] { "cmd" }, true)] + [InlineData(new string[0], new[] { "arg1" }, new[] { "cmd", "--opt1" }, true)] + [InlineData(new string[0], new[] { "arg1" }, new[] { "cmd", "foo", "bar" }, true)] + [InlineData(new string[0], new[] { "arg1" }, new[] { "cmd", "foo" }, false)] + public static void Run_PrintsCommandUsage_WhenUnexpectedArgumentsAreProvided( string[] optionNames, string[] argNames, string[] providedArgs, @@ -107,4 +107,4 @@ public void Run_PrintsCommandUsage_WhenUnexpectedArgumentsAreProvided( Assert.Empty(output.ToString()); } } -} \ No newline at end of file +} diff --git a/test/Swashbuckle.AspNetCore.Cli.Test/Swashbuckle.AspNetCore.Cli.Test.csproj b/test/Swashbuckle.AspNetCore.Cli.Test/Swashbuckle.AspNetCore.Cli.Test.csproj index 2ccd4ea7fb..66198fe569 100644 --- a/test/Swashbuckle.AspNetCore.Cli.Test/Swashbuckle.AspNetCore.Cli.Test.csproj +++ b/test/Swashbuckle.AspNetCore.Cli.Test/Swashbuckle.AspNetCore.Cli.Test.csproj @@ -8,6 +8,7 @@ + diff --git a/test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs b/test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs index cc6598009a..e056a9a0f2 100644 --- a/test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs +++ b/test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs @@ -1,26 +1,32 @@ -using System.IO; +using System; +using System.IO; using System.Text.Json; using Swashbuckle.AspNetCore.TestSupport.Utilities; using Xunit; namespace Swashbuckle.AspNetCore.Cli.Test { - public class ToolTests + public static class ToolTests { [Fact] - public void Throws_When_Startup_Assembly_Does_Not_Exist() + public static void Throws_When_Startup_Assembly_Does_Not_Exist() { - var args = new string[] { "tofile", "--output", "swagger.json", "--serializeasv2", "./does_not_exist.dll", "v1" }; + string[] args = ["tofile", "--output", "swagger.json", "--serializeasv2", "./does_not_exist.dll", "v1"]; Assert.Throws(() => Program.Main(args)); } [Fact] - public void Can_Generate_Swagger_Json() + public static void Can_Generate_Swagger_Json() { - using var temporaryDirectory = new TemporaryDirectory(); - var args = new string[] { "tofile", "--output", $"{temporaryDirectory.Path}/swagger.json", "--serializeasv2", Path.Combine(Directory.GetCurrentDirectory(), "Basic.dll"), "v1" }; - Assert.Equal(0, Program.Main(args)); - using var document = JsonDocument.Parse(File.ReadAllText(Path.Combine(temporaryDirectory.Path, "swagger.json"))); + using var document = RunApplication((outputPath) => + [ + "tofile", + "--output", + outputPath, + "--serializeasv2", + Path.Combine(Directory.GetCurrentDirectory(), "Basic.dll"), + "v1" + ]); // verify one of the endpoints var paths = document.RootElement.GetProperty("paths"); @@ -29,20 +35,22 @@ public void Can_Generate_Swagger_Json() } [Fact] - public void Overwrites_Existing_File() + public static void Overwrites_Existing_File() { - using var temporaryDirectory = new TemporaryDirectory(); - var path = Path.Combine(temporaryDirectory.Path, "swagger.json"); - - var dummyContent = new string('x', 100_000); - File.WriteAllText(path, dummyContent); - - var args = new string[] { "tofile", "--output", path, Path.Combine(Directory.GetCurrentDirectory(), "Basic.dll"), "v1" }; - Assert.Equal(0, Program.Main(args)); - - var readContent = File.ReadAllText(path); - Assert.True(readContent.Length < dummyContent.Length); - using var document = JsonDocument.Parse(readContent); + using var document = RunApplication((outputPath) => + { + File.WriteAllText(outputPath, new string('x', 100_000)); + + return + [ + "tofile", + "--output", + outputPath, + "--serializeasv2", + Path.Combine(Directory.GetCurrentDirectory(), "Basic.dll"), + "v1" + ]; + }); // verify one of the endpoints var paths = document.RootElement.GetProperty("paths"); @@ -51,13 +59,17 @@ public void Overwrites_Existing_File() } [Fact] - public void CustomDocumentSerializer_Writes_Custom_V2_Document() + public static void CustomDocumentSerializer_Writes_Custom_V2_Document() { - using var temporaryDirectory = new TemporaryDirectory(); - var args = new string[] { "tofile", "--output", $"{temporaryDirectory.Path}/swagger.json", "--serializeasv2", Path.Combine(Directory.GetCurrentDirectory(), "CustomDocumentSerializer.dll"), "v1" }; - Assert.Equal(0, Program.Main(args)); - - using var document = JsonDocument.Parse(File.ReadAllText(Path.Combine(temporaryDirectory.Path, "swagger.json"))); + using var document = RunApplication((outputPath) => + [ + "tofile", + "--output", + outputPath, + "--serializeasv2", + Path.Combine(Directory.GetCurrentDirectory(), "CustomDocumentSerializer.dll"), + "v1" + ]); // verify that the custom serializer wrote the swagger info var swaggerInfo = document.RootElement.GetProperty("swagger").GetString(); @@ -65,34 +77,71 @@ public void CustomDocumentSerializer_Writes_Custom_V2_Document() } [Fact] - public void CustomDocumentSerializer_Writes_Custom_V3_Document() + public static void CustomDocumentSerializer_Writes_Custom_V3_Document() { - using var temporaryDirectory = new TemporaryDirectory(); - var args = new string[] { "tofile", "--output", $"{temporaryDirectory.Path}/swagger.json", Path.Combine(Directory.GetCurrentDirectory(), "CustomDocumentSerializer.dll"), "v1" }; - Assert.Equal(0, Program.Main(args)); - - using var document = JsonDocument.Parse(File.ReadAllText(Path.Combine(temporaryDirectory.Path, "swagger.json"))); + using var document = RunApplication((outputPath) => + [ + "tofile", + "--output", + outputPath, + Path.Combine(Directory.GetCurrentDirectory(), + "CustomDocumentSerializer.dll"), + "v1" + ]); // verify that the custom serializer wrote the swagger info var swaggerInfo = document.RootElement.GetProperty("swagger").GetString(); Assert.Equal("DocumentSerializerTest3.0", swaggerInfo); } -#if NET6_0_OR_GREATER [Fact] - public void Can_Generate_Swagger_Json_ForTopLevelApp() + public static void Can_Generate_Swagger_Json_ForTopLevelApp() { - using var temporaryDirectory = new TemporaryDirectory(); - var args = new string[] { "tofile", "--output", $"{temporaryDirectory.Path}/swagger.json", "--serializeasv2", Path.Combine(Directory.GetCurrentDirectory(), "MinimalApp.dll"), "v1" }; - Assert.Equal(0, Program.Main(args)); - - using var document = JsonDocument.Parse(File.ReadAllText(Path.Combine(temporaryDirectory.Path, "swagger.json"))); + using var document = RunApplication((outputPath) => + [ + "tofile", + "--output", + outputPath, + "--serializeasv2", + Path.Combine(Directory.GetCurrentDirectory(), "MinimalApp.dll"), + "v1" + ]); // verify one of the endpoints var paths = document.RootElement.GetProperty("paths"); var path = paths.GetProperty("/WeatherForecast"); Assert.True(path.TryGetProperty("get", out _)); } -#endif + + [Fact] + public static void Does_Not_Run_Crashing_HostedService() + { + using var document = RunApplication((outputPath) => + [ + "tofile", + "--output", + outputPath, + Path.Combine(Directory.GetCurrentDirectory(), "MinimalAppWithHostedServices.dll"), + "v1" + ]); + + // verify one of the endpoints + var paths = document.RootElement.GetProperty("paths"); + var path = paths.GetProperty("/ShouldContain"); + Assert.True(path.TryGetProperty("get", out _)); + } + + private static JsonDocument RunApplication(Func setup) + { + using var temporaryDirectory = new TemporaryDirectory(); + string outputPath = Path.Combine(temporaryDirectory.Path, "swagger.json"); + + string[] args = setup(outputPath); + + Assert.Equal(0, Program.Main(args)); + + string json = File.ReadAllText(outputPath); + return JsonDocument.Parse(json); + } } } diff --git a/test/WebSites/MinimalAppWithHostedServices/MinimalAppWithHostedServices.csproj b/test/WebSites/MinimalAppWithHostedServices/MinimalAppWithHostedServices.csproj new file mode 100644 index 0000000000..88afa5f5a3 --- /dev/null +++ b/test/WebSites/MinimalAppWithHostedServices/MinimalAppWithHostedServices.csproj @@ -0,0 +1,18 @@ + + + + net6.0;net8.0 + enable + enable + $([System.IO.Path]::Combine('$(ArtifactsPath)', 'bin', 'Swashbuckle.AspNetCore.Cli', '$(Configuration)_$(TargetFramework)', 'dotnet-swagger.dll')) + + + + + + + + + + + diff --git a/test/WebSites/MinimalAppWithHostedServices/Program.cs b/test/WebSites/MinimalAppWithHostedServices/Program.cs new file mode 100644 index 0000000000..60eade8971 --- /dev/null +++ b/test/WebSites/MinimalAppWithHostedServices/Program.cs @@ -0,0 +1,34 @@ +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(c => +{ + c.SwaggerDoc("v1", new() { Title = "MinimalApp", Version = "v1" }); +}); + +builder.Services.AddHostedService(); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "MinimalApp v1")); +} + +app.MapGet("/ShouldContain", () => "Hello World!"); + +app.Run(); + +class HostedService : IHostedService +{ + public Task StartAsync(CancellationToken cancellationToken) + { + throw new Exception("Crash!"); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } +} diff --git a/test/WebSites/MinimalAppWithHostedServices/Properties/launchSettings.json b/test/WebSites/MinimalAppWithHostedServices/Properties/launchSettings.json new file mode 100644 index 0000000000..633d47ed49 --- /dev/null +++ b/test/WebSites/MinimalAppWithHostedServices/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "MinimalAppWithHostedServices": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:57095;http://localhost:57096" + } + } +}