diff --git a/.editorconfig b/.editorconfig
index fe3bd5d..5209be7 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -14,10 +14,12 @@ spelling_exclusion_path = SpellingExclusions.dic
indent_size = 4
insert_final_newline = true
charset = utf-8-bom
+end_of_line = lf
# XML project files
[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}]
-indent_size = 4
+indent_size = 2
+end_of_line = lf
# XML config files
[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..07764a7
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+* text eol=lf
\ No newline at end of file
diff --git a/Saunter.sln b/Saunter.sln
index 4627766..bd69f05 100644
--- a/Saunter.sln
+++ b/Saunter.sln
@@ -18,6 +18,7 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E0D34C77-924E-4F6B-9289-5A2F07D125A8}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
+ .gitattributes = .gitattributes
CHANGELOG.md = CHANGELOG.md
.github\workflows\ci.yaml = .github\workflows\ci.yaml
README.md = README.md
@@ -28,6 +29,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Saunter.IntegrationTests.Re
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Saunter.Tests.MarkerTypeTests", "test\Saunter.Tests.MarkerTypeTests\Saunter.Tests.MarkerTypeTests.csproj", "{02284473-6DE7-4EE0-8433-2AC295045549}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AsyncAPI.Saunter.Generator.Cli", "src\AsyncApi.Saunter.Generator.Cli\AsyncAPI.Saunter.Generator.Cli.csproj", "{6C102D4D-3DA4-4763-B75E-C15E33E7E94A}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -98,6 +101,18 @@ Global
{02284473-6DE7-4EE0-8433-2AC295045549}.Release|x64.Build.0 = Release|Any CPU
{02284473-6DE7-4EE0-8433-2AC295045549}.Release|x86.ActiveCfg = Release|Any CPU
{02284473-6DE7-4EE0-8433-2AC295045549}.Release|x86.Build.0 = Release|Any CPU
+ {6C102D4D-3DA4-4763-B75E-C15E33E7E94A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6C102D4D-3DA4-4763-B75E-C15E33E7E94A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6C102D4D-3DA4-4763-B75E-C15E33E7E94A}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {6C102D4D-3DA4-4763-B75E-C15E33E7E94A}.Debug|x64.Build.0 = Debug|Any CPU
+ {6C102D4D-3DA4-4763-B75E-C15E33E7E94A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {6C102D4D-3DA4-4763-B75E-C15E33E7E94A}.Debug|x86.Build.0 = Debug|Any CPU
+ {6C102D4D-3DA4-4763-B75E-C15E33E7E94A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6C102D4D-3DA4-4763-B75E-C15E33E7E94A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6C102D4D-3DA4-4763-B75E-C15E33E7E94A}.Release|x64.ActiveCfg = Release|Any CPU
+ {6C102D4D-3DA4-4763-B75E-C15E33E7E94A}.Release|x64.Build.0 = Release|Any CPU
+ {6C102D4D-3DA4-4763-B75E-C15E33E7E94A}.Release|x86.ActiveCfg = Release|Any CPU
+ {6C102D4D-3DA4-4763-B75E-C15E33E7E94A}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -108,6 +123,7 @@ Global
{F188D4A7-BBCB-464F-A370-2BD84D18EA79} = {6ABD4842-47AF-49A5-B057-0EBA64416789}
{7CD09B89-130A-41AF-ADAE-2166C4ED695B} = {6491E321-2D02-44AB-9116-D722FE169595}
{02284473-6DE7-4EE0-8433-2AC295045549} = {6491E321-2D02-44AB-9116-D722FE169595}
+ {6C102D4D-3DA4-4763-B75E-C15E33E7E94A} = {28D4C365-FDED-49AE-A97D-36202E24A55A}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {2F85D9DA-DBCF-4F13-8C42-5719F1469B2E}
diff --git a/src/AsyncApi.Saunter.Generator.Cli/Args.cs b/src/AsyncApi.Saunter.Generator.Cli/Args.cs
new file mode 100644
index 0000000..243315d
--- /dev/null
+++ b/src/AsyncApi.Saunter.Generator.Cli/Args.cs
@@ -0,0 +1,12 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+// ReSharper disable once CheckNamespace
+public static partial class Program
+{
+ internal const string StartupAssemblyArgument = "startupassembly";
+ internal const string DocArgument = "doc";
+ internal const string FormatOption = "--format";
+ internal const string OutputOption = "--output";
+}
diff --git a/src/AsyncApi.Saunter.Generator.Cli/AsyncApi.Saunter.Generator.Cli.csproj b/src/AsyncApi.Saunter.Generator.Cli/AsyncApi.Saunter.Generator.Cli.csproj
new file mode 100644
index 0000000..bf10ee0
--- /dev/null
+++ b/src/AsyncApi.Saunter.Generator.Cli/AsyncApi.Saunter.Generator.Cli.csproj
@@ -0,0 +1,31 @@
+
+
+
+ net8.0
+ Exe
+ enable
+ 12
+ AsyncAPI.Saunter.Generator.Cli
+
+ AsyncAPI Command Line Tools
+ Exe
+ true
+ AsyncAPI.Saunter.Generator.Cli
+
+ asyncapi
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/AsyncApi.Saunter.Generator.Cli/Commands/Tofile.cs b/src/AsyncApi.Saunter.Generator.Cli/Commands/Tofile.cs
new file mode 100644
index 0000000..dda464f
--- /dev/null
+++ b/src/AsyncApi.Saunter.Generator.Cli/Commands/Tofile.cs
@@ -0,0 +1,54 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Diagnostics;
+using System.Reflection;
+using static Program;
+
+namespace AsyncApi.Saunter.Generator.Cli.Commands;
+
+internal class Tofile
+{
+ internal static Func, int> Run(string[] args) => namedArgs =>
+ {
+ if (!File.Exists(namedArgs[StartupAssemblyArgument]))
+ {
+ throw new FileNotFoundException(namedArgs[StartupAssemblyArgument]);
+ }
+
+ var depsFile = namedArgs[StartupAssemblyArgument].Replace(".dll", ".deps.json");
+ var runtimeConfig = namedArgs[StartupAssemblyArgument].Replace(".dll", ".runtimeconfig.json");
+ var commandName = args[0];
+
+ var subProcessArguments = new string[args.Length - 1];
+ if (subProcessArguments.Length > 0)
+ {
+ Array.Copy(args, 1, subProcessArguments, 0, subProcessArguments.Length);
+ }
+
+ var subProcessCommandLine =
+ $"exec --depsfile {EscapePath(depsFile)} " +
+ $"--runtimeconfig {EscapePath(runtimeConfig)} " +
+ $"--additional-deps AsyncAPI.Saunter.Generator.Cli.deps.json " +
+ //$"--additionalprobingpath {EscapePath(typeof(Program).GetTypeInfo().Assembly.Location)} " +
+ $"{EscapePath(typeof(Program).GetTypeInfo().Assembly.Location)} " +
+ $"_{commandName} {string.Join(" ", subProcessArguments.Select(EscapePath))}";
+
+ try
+ {
+ var subProcess = Process.Start("dotnet", subProcessCommandLine);
+ subProcess.WaitForExit();
+ return subProcess.ExitCode;
+ }
+ catch (Exception e)
+ {
+ throw new Exception("Running internal _tofile failed.", e);
+ }
+ };
+
+ private static string EscapePath(string path)
+ {
+ return path.Contains(' ') ? "\"" + path + "\"" : path;
+ }
+}
diff --git a/src/AsyncApi.Saunter.Generator.Cli/Commands/TofileInternal.cs b/src/AsyncApi.Saunter.Generator.Cli/Commands/TofileInternal.cs
new file mode 100644
index 0000000..616769b
--- /dev/null
+++ b/src/AsyncApi.Saunter.Generator.Cli/Commands/TofileInternal.cs
@@ -0,0 +1,183 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using LEGO.AsyncAPI.Readers;
+using Microsoft.Extensions.Options;
+using Saunter.Serialization;
+using Saunter;
+using System.Runtime.Loader;
+using System.Reflection;
+using LEGO.AsyncAPI;
+using LEGO.AsyncAPI.Models;
+using Microsoft.Extensions.DependencyInjection;
+using AsyncApi.Saunter.Generator.Cli.SwashbuckleImport;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore;
+using Microsoft.Extensions.Hosting;
+using static Program;
+
+namespace AsyncApi.Saunter.Generator.Cli.Commands;
+
+internal class TofileInternal
+{
+ internal static int Run(IDictionary namedArgs)
+ {
+ // 1) Configure host with provided startupassembly
+ var startupAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.Combine(Directory.GetCurrentDirectory(), namedArgs[StartupAssemblyArgument]));
+
+ // 2) Build a service container that's based on the startup assembly
+ var serviceProvider = GetServiceProvider(startupAssembly);
+
+ // 3) Retrieve AsyncAPI via configured provider
+ var documentProvider = serviceProvider.GetService();
+ var asyncapiOptions = serviceProvider.GetService>();
+ var documentSerializer = serviceProvider.GetRequiredService();
+
+ if (!asyncapiOptions.Value.NamedApis.TryGetValue(namedArgs[DocArgument], out var prototype))
+ {
+ throw new ArgumentOutOfRangeException(DocArgument, namedArgs[DocArgument], $"Requested AsyncAPI document not found: '{namedArgs[DocArgument]}'. Known document(s): {string.Join(", ", asyncapiOptions.Value.NamedApis.Keys)}.");
+ }
+ var asyncApiSchema = documentProvider.GetDocument(asyncapiOptions.Value, prototype);
+ var asyncApiSchemaJson = documentSerializer.Serialize(asyncApiSchema);
+ var asyncApiDocument = new AsyncApiStringReader().Read(asyncApiSchemaJson, out var diagnostic);
+ if (diagnostic.Errors.Any())
+ {
+ Console.Error.WriteLine($"AsyncAPI Schema is not valid ({diagnostic.Errors.Count} Error(s), {diagnostic.Warnings.Count} Warning(s)):" +
+ $"{Environment.NewLine}{string.Join(Environment.NewLine, diagnostic.Errors.Select(x => $"- {x}"))}");
+ }
+
+ // 4) Serialize to specified output location or stdout
+ var outputPath = namedArgs.TryGetValue(OutputOption, out var arg1) ? Path.Combine(Directory.GetCurrentDirectory(), arg1) : null;
+
+ if (!string.IsNullOrEmpty(outputPath))
+ {
+ var directoryPath = Path.GetDirectoryName(outputPath);
+ if (!string.IsNullOrEmpty(directoryPath) && !Directory.Exists(directoryPath))
+ {
+ Directory.CreateDirectory(directoryPath);
+ }
+ }
+
+ var exportJson = true;
+ var exportYml = false;
+ var exportYaml = false;
+ if (namedArgs.TryGetValue(FormatOption, out var format))
+ {
+ var splitted = format.Split(',').Select(x => x.Trim()).ToList();
+ exportJson = splitted.Any(x => x.Equals("json", StringComparison.OrdinalIgnoreCase));
+ exportYml = splitted.Any(x => x.Equals("yml", StringComparison.OrdinalIgnoreCase));
+ exportYaml = splitted.Any(x => x.Equals("yaml", StringComparison.OrdinalIgnoreCase));
+ }
+
+ if (exportJson)
+ {
+ WriteFile(AddFileExtension(outputPath, "json"), stream => asyncApiDocument.SerializeAsJson(stream, AsyncApiVersion.AsyncApi2_0));
+ }
+
+ if (exportYml)
+ {
+ WriteFile(AddFileExtension(outputPath, "yml"), stream => asyncApiDocument.SerializeAsYaml(stream, AsyncApiVersion.AsyncApi2_0));
+ }
+
+ if (exportYaml)
+ {
+ WriteFile(AddFileExtension(outputPath, "yaml"), stream => asyncApiDocument.SerializeAsYaml(stream, AsyncApiVersion.AsyncApi2_0));
+ }
+
+ return 0;
+ }
+
+ private static void WriteFile(string outputPath, Action writeAction)
+ {
+ using var stream = outputPath != null ? File.Create(outputPath) : Console.OpenStandardOutput();
+ writeAction(stream);
+
+ if (outputPath != null)
+ {
+ Console.WriteLine($"AsyncAPI {Path.GetExtension(outputPath)[1..]} successfully written to {outputPath}");
+ }
+ }
+
+ private static string AddFileExtension(string outputPath, string extension)
+ {
+ if (outputPath == null)
+ {
+ return outputPath;
+ }
+
+ if (outputPath.EndsWith(extension, StringComparison.OrdinalIgnoreCase))
+ {
+ return outputPath;
+ }
+
+ return $"{TrimEnd(outputPath, ".json", ".yml", ".yaml")}.{extension}";
+ }
+
+ private static string TrimEnd(string str, params string[] trims)
+ {
+ foreach (var trim in trims)
+ {
+ if (str.EndsWith(trim, StringComparison.OrdinalIgnoreCase))
+ {
+ str = str[..^trim.Length];
+ }
+ }
+ return str;
+ }
+
+ private static IServiceProvider GetServiceProvider(Assembly startupAssembly)
+ {
+ if (TryGetCustomHost(startupAssembly, "AsyncAPIHostFactory", "CreateHost", out IHost host))
+ {
+ return host.Services;
+ }
+
+ if (TryGetCustomHost(startupAssembly, "AsyncAPIWebHostFactory", "CreateWebHost", out IWebHost webHost))
+ {
+ return webHost.Services;
+ }
+
+ try
+ {
+ return WebHost.CreateDefaultBuilder().UseStartup(startupAssembly.GetName().Name).Build().Services;
+ }
+ catch
+ {
+ var serviceProvider = HostingApplication.GetServiceProvider(startupAssembly);
+
+ if (serviceProvider != null)
+ {
+ return serviceProvider;
+ }
+
+ throw;
+ }
+ }
+
+ private static bool TryGetCustomHost(Assembly startupAssembly, string factoryClassName, string factoryMethodName, out THost host)
+ {
+ // Scan the assembly for any types that match the provided naming convention
+ var factoryTypes = startupAssembly.DefinedTypes.Where(t => t.Name == factoryClassName).ToList();
+
+ if (factoryTypes.Count == 0)
+ {
+ host = default;
+ return false;
+ }
+ else if (factoryTypes.Count > 1)
+ {
+ throw new InvalidOperationException($"Multiple {factoryClassName} classes detected");
+ }
+
+ var factoryMethod = factoryTypes.Single().GetMethod(factoryMethodName, BindingFlags.Public | BindingFlags.Static);
+
+ if (factoryMethod == null || factoryMethod.ReturnType != typeof(THost))
+ {
+ throw new InvalidOperationException($"{factoryClassName} class detected but does not contain a public static method called {factoryMethodName} with return type {typeof(THost).Name}");
+ }
+
+ host = (THost)factoryMethod.Invoke(null, null);
+ return true;
+ }
+}
diff --git a/src/AsyncApi.Saunter.Generator.Cli/Program.cs b/src/AsyncApi.Saunter.Generator.Cli/Program.cs
new file mode 100644
index 0000000..33471b7
--- /dev/null
+++ b/src/AsyncApi.Saunter.Generator.Cli/Program.cs
@@ -0,0 +1,32 @@
+using AsyncApi.Saunter.Generator.Cli.Commands;
+using AsyncApi.Saunter.Generator.Cli.SwashbuckleImport;
+
+// Helper to simplify command line parsing etc.
+var runner = new CommandRunner("dotnet asyncapi", "AsyncAPI Command Line Tools", Console.Out);
+
+// NOTE: The "dotnet asyncapi tofile" command does not serve the request directly. Instead, it invokes a corresponding
+// command (called _tofile) via "dotnet exec" so that the runtime configuration (*.runtimeconfig & *.deps.json) of the
+// provided startupassembly can be used instead of the tool's. This is neccessary to successfully load the
+// startupassembly and it's transitive dependencies. See https://github.com/dotnet/coreclr/issues/13277 for more.
+
+// > dotnet asyncapi tofile ...
+runner.SubCommand("tofile", "retrieves AsyncAPI from a startup assembly, and writes to file ", c =>
+{
+ c.Argument(StartupAssemblyArgument, "relative path to the application's startup assembly");
+ c.Argument(DocArgument, "name of the AsyncAPI doc you want to retrieve, as configured in your startup class");
+ c.Option(OutputOption, "relative path where the AsyncAPI will be output, defaults to stdout");
+ c.Option(FormatOption, "exports AsyncAPI in json and/or yml format [Default json]");
+ c.OnRun(Tofile.Run(args));
+});
+
+// > dotnet asyncapi _tofile ... (* should only be invoked via "dotnet exec")
+runner.SubCommand("_tofile", "", c =>
+{
+ c.Argument(StartupAssemblyArgument, "");
+ c.Argument(DocArgument, "");
+ c.Option(OutputOption, "");
+ c.Option(FormatOption, "");
+ c.OnRun(TofileInternal.Run);
+});
+
+return runner.Run(args);
diff --git a/src/AsyncApi.Saunter.Generator.Cli/SwashbuckleImport/CommandRunner.cs b/src/AsyncApi.Saunter.Generator.Cli/SwashbuckleImport/CommandRunner.cs
new file mode 100644
index 0000000..c3c8eca
--- /dev/null
+++ b/src/AsyncApi.Saunter.Generator.Cli/SwashbuckleImport/CommandRunner.cs
@@ -0,0 +1,145 @@
+namespace AsyncApi.Saunter.Generator.Cli.SwashbuckleImport;
+
+internal class CommandRunner
+{
+ private readonly Dictionary _argumentDescriptors;
+ private readonly Dictionary _optionDescriptors;
+ private Func, int> _runFunc;
+ private readonly List _subRunners;
+ private readonly TextWriter _output;
+
+ public CommandRunner(string commandName, string commandDescription, TextWriter output)
+ {
+ CommandName = commandName;
+ CommandDescription = commandDescription;
+ _argumentDescriptors = [];
+ _optionDescriptors = [];
+ _runFunc = (_) => 1; // no-op
+ _subRunners = [];
+ _output = output;
+ }
+
+ public string CommandName { get; private set; }
+
+ public string CommandDescription { get; private set; }
+
+ public void Argument(string name, string description)
+ {
+ _argumentDescriptors.Add(name, description);
+ }
+
+ public void Option(string name, string description, bool isFlag = false)
+ {
+ if (!name.StartsWith("--")) throw new ArgumentException("name of option must begin with --");
+ _optionDescriptors.Add(name, new OptionDescriptor { Description = description, IsFlag = isFlag });
+ }
+
+ public void OnRun(Func, int> runFunc)
+ {
+ _runFunc = runFunc;
+ }
+
+ public void SubCommand(string name, string description, Action configAction)
+ {
+ var runner = new CommandRunner($"{CommandName} {name}", description, _output);
+ configAction(runner);
+ _subRunners.Add(runner);
+ }
+
+ public int Run(IEnumerable args)
+ {
+ if (args.Any())
+ {
+ var subRunner = _subRunners.FirstOrDefault(r => r.CommandName.Split(' ').Last() == args.First());
+ if (subRunner != null) return subRunner.Run(args.Skip(1));
+ }
+
+ if (_subRunners.Any() || !TryParseArgs(args, out IDictionary namedArgs))
+ {
+ PrintUsage();
+ return 1;
+ }
+
+ return _runFunc(namedArgs);
+ }
+
+ private bool TryParseArgs(IEnumerable args, out IDictionary namedArgs)
+ {
+ namedArgs = new Dictionary();
+ var argsQueue = new Queue(args);
+
+ // Process options first
+ while (argsQueue.Any() && argsQueue.Peek().StartsWith("--"))
+ {
+ // Ensure it's a known option
+ var name = argsQueue.Dequeue();
+ if (!_optionDescriptors.TryGetValue(name, out OptionDescriptor optionDescriptor))
+ return false;
+
+ // If it's not a flag, ensure it's followed by a corresponding value
+ if (!optionDescriptor.IsFlag && (!argsQueue.Any() || argsQueue.Peek().StartsWith("--")))
+ return false;
+
+ namedArgs.Add(name, (!optionDescriptor.IsFlag ? argsQueue.Dequeue() : null));
+ }
+
+ // Process required args - ensure corresponding values are provided
+ foreach (var name in _argumentDescriptors.Keys)
+ {
+ if (!argsQueue.Any() || argsQueue.Peek().StartsWith("--")) return false;
+ namedArgs.Add(name, argsQueue.Dequeue());
+ }
+
+ return argsQueue.Count() == 0;
+ }
+
+ private void PrintUsage()
+ {
+ if (_subRunners.Any())
+ {
+ // List sub commands
+ _output.WriteLine(CommandDescription);
+ _output.WriteLine("Commands:");
+ foreach (var runner in _subRunners)
+ {
+ var shortName = runner.CommandName.Split(' ').Last();
+ if (shortName.StartsWith("_")) continue; // convention to hide commands
+ _output.WriteLine($" {shortName}: {runner.CommandDescription}");
+ }
+ _output.WriteLine();
+ }
+ else
+ {
+ // Usage for this command
+ var optionsPart = _optionDescriptors.Any() ? "[options] " : "";
+ var argParts = _argumentDescriptors.Keys.Select(name => $"[{name}]");
+ _output.WriteLine($"Usage: {CommandName} {optionsPart}{string.Join(" ", argParts)}");
+ _output.WriteLine();
+
+ // Arguments
+ foreach (var entry in _argumentDescriptors)
+ {
+ _output.WriteLine($"{entry.Key}:");
+ _output.WriteLine($" {entry.Value}");
+ _output.WriteLine();
+ }
+
+ // Options
+ if (_optionDescriptors.Any())
+ {
+ _output.WriteLine("options:");
+ foreach (var entry in _optionDescriptors)
+ {
+ _output.WriteLine($" {entry.Key}: {entry.Value.Description}");
+ }
+ _output.WriteLine();
+ }
+ }
+ }
+
+ private struct OptionDescriptor
+ {
+ public string Description;
+ public bool IsFlag;
+ }
+}
diff --git a/src/AsyncApi.Saunter.Generator.Cli/SwashbuckleImport/HostFactoryResolver.cs b/src/AsyncApi.Saunter.Generator.Cli/SwashbuckleImport/HostFactoryResolver.cs
new file mode 100644
index 0000000..29d3e96
--- /dev/null
+++ b/src/AsyncApi.Saunter.Generator.Cli/SwashbuckleImport/HostFactoryResolver.cs
@@ -0,0 +1,325 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics;
+using System.Reflection;
+
+namespace Microsoft.Extensions.Hosting;
+
+internal sealed class HostFactoryResolver
+{
+ private const BindingFlags DeclaredOnlyLookup = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly;
+
+ public const string BuildWebHost = nameof(BuildWebHost);
+ public const string CreateWebHostBuilder = nameof(CreateWebHostBuilder);
+ public const string CreateHostBuilder = nameof(CreateHostBuilder);
+
+ // The amount of time we wait for the diagnostic source events to fire
+ private static readonly TimeSpan s_defaultWaitTimeout = Debugger.IsAttached ? Timeout.InfiniteTimeSpan : TimeSpan.FromSeconds(30);
+
+ public static Func ResolveWebHostFactory(Assembly assembly)
+ {
+ return ResolveFactory(assembly, BuildWebHost);
+ }
+
+ public static Func ResolveWebHostBuilderFactory(Assembly assembly)
+ {
+ return ResolveFactory(assembly, CreateWebHostBuilder);
+ }
+
+ public static Func ResolveHostBuilderFactory(Assembly assembly)
+ {
+ return ResolveFactory(assembly, CreateHostBuilder);
+ }
+
+ // This helpers encapsulates all of the complex logic required to:
+ // 1. Execute the entry point of the specified assembly in a different thread.
+ // 2. Wait for the diagnostic source events to fire
+ // 3. Give the caller a chance to execute logic to mutate the IHostBuilder
+ // 4. Resolve the instance of the applications's IHost
+ // 5. Allow the caller to determine if the entry point has completed
+ public static Func ResolveHostFactory(Assembly assembly,
+ TimeSpan? waitTimeout = null,
+ bool stopApplication = true,
+ Action