diff --git a/src/libraries/Microsoft.Extensions.HostFactoryResolver/src/HostFactoryResolver.cs b/src/libraries/Microsoft.Extensions.HostFactoryResolver/src/HostFactoryResolver.cs index 9b3a30734eb2ab..16e1fe40dc8366 100644 --- a/src/libraries/Microsoft.Extensions.HostFactoryResolver/src/HostFactoryResolver.cs +++ b/src/libraries/Microsoft.Extensions.HostFactoryResolver/src/HostFactoryResolver.cs @@ -2,7 +2,13 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; +using System.Diagnostics; using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; #nullable enable @@ -16,6 +22,9 @@ internal sealed class HostFactoryResolver 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 = TimeSpan.FromSeconds(5); + public static Func? ResolveWebHostFactory(Assembly assembly) { return ResolveFactory(assembly, BuildWebHost); @@ -31,6 +40,35 @@ internal sealed class HostFactoryResolver return ResolveFactory(assembly, CreateHostBuilder); } + public static Func? ResolveHostFactory(Assembly assembly, TimeSpan? waitTimeout = null, bool stopApplication = true, Action? configureHostBuilder = null) + { + if (assembly.EntryPoint is null) + { + return null; + } + + try + { + // Attempt to load hosting and check the version to make sure the events + // even have a change of firing (they were adding in .NET >= 6) + var hostingAssembly = Assembly.Load("Microsoft.Extensions.Hosting"); + if (hostingAssembly.GetName().Version is Version version && version.Major < 6) + { + return null; + } + + // We're using a version >= 6 so the events can fire. If they don't fire + // then it's because the application isn't using the hosting APIs + } + catch + { + // There was an error loading the extensions assembly, return null. + return null; + } + + return args => new HostingListener(args, assembly.EntryPoint, waitTimeout ?? s_defaultWaitTimeout, stopApplication, configureHostBuilder).CreateHost(); + } + private static Func? ResolveFactory(Assembly assembly, string name) { var programType = assembly?.EntryPoint?.DeclaringType; @@ -58,7 +96,7 @@ private static bool IsFactory(MethodInfo? factory) } // Used by EF tooling without any Hosting references. Looses some return type safety checks. - public static Func? ResolveServiceProviderFactory(Assembly assembly) + public static Func? ResolveServiceProviderFactory(Assembly assembly, TimeSpan? waitTimeout = null) { // Prefer the older patterns by default for back compat. var webHostFactory = ResolveWebHostFactory(assembly); @@ -93,6 +131,16 @@ private static bool IsFactory(MethodInfo? factory) }; } + var hostFactory = ResolveHostFactory(assembly, waitTimeout: waitTimeout); + if (hostFactory != null) + { + return args => + { + var host = hostFactory(args); + return GetServiceProvider(host); + }; + } + return null; } @@ -112,5 +160,133 @@ private static bool IsFactory(MethodInfo? factory) var servicesProperty = hostType.GetProperty("Services", DeclaredOnlyLookup); return (IServiceProvider?)servicesProperty?.GetValue(host); } + + private class HostingListener : IObserver, IObserver> + { + private readonly string[] _args; + private readonly MethodInfo _entryPoint; + private readonly TimeSpan _waitTimeout; + private readonly bool _stopApplication; + + private readonly TaskCompletionSource _hostTcs = new(); + private IDisposable? _disposable; + private Action? _configure; + + public HostingListener(string[] args, MethodInfo entryPoint, TimeSpan waitTimeout, bool stopApplication, Action? configure) + { + _args = args; + _entryPoint = entryPoint; + _waitTimeout = waitTimeout; + _stopApplication = stopApplication; + _configure = configure; + } + + public object CreateHost() + { + 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(() => + { + try + { + 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) + { + // The host was stopped by our own logic + } + catch (TargetInvocationException tie) + { + // Another exception happened, propagate that to the caller + _hostTcs.TrySetException(tie.InnerException ?? tie); + } + catch (Exception ex) + { + // Another exception happened, propagate that to the caller + _hostTcs.TrySetException(ex); + } + }) + { + // Make sure this doesn't hang the process + IsBackground = true + }; + + // Start the thread + thread.Start(); + + 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() + { + _disposable?.Dispose(); + } + + public void OnError(Exception error) + { + + } + + public void OnNext(DiagnosticListener value) + { + if (value.Name == "Microsoft.Extensions.Hosting") + { + _disposable = value.Subscribe(this); + } + } + + public void OnNext(KeyValuePair value) + { + if (value.Key == "HostBuilding") + { + _configure?.Invoke(value.Value!); + } + + if (value.Key == "HostBuilt") + { + _hostTcs.TrySetResult(value.Value!); + + if (_stopApplication) + { + // Stop the host from running further + throw new StopTheHostException(); + } + } + } + + private class StopTheHostException : Exception + { + + } + } } } diff --git a/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/CreateHostBuilderInvalidSignature/Program.cs b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/CreateHostBuilderInvalidSignature/Program.cs index b1b233eb95d519..f82816eed4537b 100644 --- a/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/CreateHostBuilderInvalidSignature/Program.cs +++ b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/CreateHostBuilderInvalidSignature/Program.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using MockHostTypes; +using Microsoft.Extensions.Hosting; namespace CreateHostBuilderInvalidSignature { @@ -9,7 +10,7 @@ public class Program { public static void Main(string[] args) { - var webHost = CreateHostBuilder(null, args).Build(); + var webHost = CreateHostBuilder(null, args)?.Build(); } // Extra parameter diff --git a/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/CreateHostBuilderPatternTestSite/Program.cs b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/CreateHostBuilderPatternTestSite/Program.cs index 515615731fa09c..55e872f8f34127 100644 --- a/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/CreateHostBuilderPatternTestSite/Program.cs +++ b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/CreateHostBuilderPatternTestSite/Program.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using MockHostTypes; +using Microsoft.Extensions.Hosting; namespace CreateHostBuilderPatternTestSite { diff --git a/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/HostFactoryResolverTests.cs b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/HostFactoryResolverTests.cs index 55d53fa668e355..f68836a77d0716 100644 --- a/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/HostFactoryResolverTests.cs +++ b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/HostFactoryResolverTests.cs @@ -4,12 +4,18 @@ using MockHostTypes; using System; using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using Microsoft.DotNet.RemoteExecutor; using Xunit; namespace Microsoft.Extensions.Hosting.Tests { public class HostFactoryResolverTests { + public static bool RequirementsMet => RemoteExecutor.IsSupported && PlatformDetection.IsThreadingSupported; + + private static readonly TimeSpan s_WaitTimeout = TimeSpan.FromSeconds(20); + [Fact] [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(BuildWebHostPatternTestSite.Program))] public void BuildWebHostPattern_CanFindWebHost() @@ -46,7 +52,7 @@ public void BuildWebHostPattern__Invalid_CantFindServiceProvider() { var factory = HostFactoryResolver.ResolveServiceProviderFactory(typeof(BuildWebHostInvalidSignature.Program).Assembly); - Assert.Null(factory); + Assert.NotNull(factory); } [Fact] @@ -119,13 +125,95 @@ public void CreateHostBuilderPattern__Invalid_CantFindHostBuilder() Assert.Null(factory); } - [Fact] + [ConditionalFact(nameof(RequirementsMet))] [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(CreateHostBuilderInvalidSignature.Program))] public void CreateHostBuilderPattern__Invalid_CantFindServiceProvider() { - var factory = HostFactoryResolver.ResolveServiceProviderFactory(typeof(CreateHostBuilderInvalidSignature.Program).Assembly); + using var _ = RemoteExecutor.Invoke(() => + { + var factory = HostFactoryResolver.ResolveServiceProviderFactory(typeof(CreateHostBuilderInvalidSignature.Program).Assembly, s_WaitTimeout); - Assert.Null(factory); + Assert.NotNull(factory); + Assert.Throws(() => factory(Array.Empty())); + }); + } + + [ConditionalFact(nameof(RequirementsMet))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NoSpecialEntryPointPattern.Program))] + public void NoSpecialEntryPointPattern() + { + using var _ = RemoteExecutor.Invoke(() => + { + var factory = HostFactoryResolver.ResolveServiceProviderFactory(typeof(NoSpecialEntryPointPattern.Program).Assembly, s_WaitTimeout); + + Assert.NotNull(factory); + Assert.IsAssignableFrom(factory(Array.Empty())); + }); + } + + [ConditionalFact(nameof(RequirementsMet))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NoSpecialEntryPointPatternThrows.Program))] + public void NoSpecialEntryPointPatternThrows() + { + using var _ = RemoteExecutor.Invoke(() => + { + var factory = HostFactoryResolver.ResolveServiceProviderFactory(typeof(NoSpecialEntryPointPatternThrows.Program).Assembly, s_WaitTimeout); + + Assert.NotNull(factory); + Assert.Throws(() => factory(Array.Empty())); + }); + } + + [ConditionalFact(nameof(RequirementsMet))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NoSpecialEntryPointPatternExits.Program))] + public void NoSpecialEntryPointPatternExits() + { + using var _ = RemoteExecutor.Invoke(() => + { + var factory = HostFactoryResolver.ResolveServiceProviderFactory(typeof(NoSpecialEntryPointPatternExits.Program).Assembly, s_WaitTimeout); + + Assert.NotNull(factory); + Assert.Throws(() => factory(Array.Empty())); + }); + } + + [ConditionalFact(nameof(RequirementsMet))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NoSpecialEntryPointPatternHangs.Program))] + public void NoSpecialEntryPointPatternHangs() + { + using var _ = RemoteExecutor.Invoke(() => + { + var factory = HostFactoryResolver.ResolveServiceProviderFactory(typeof(NoSpecialEntryPointPatternHangs.Program).Assembly, s_WaitTimeout); + + Assert.NotNull(factory); + Assert.Throws(() => factory(Array.Empty())); + }); + } + + [ConditionalFact(nameof(RequirementsMet))] + [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(NoSpecialEntryPointPatternMainNoArgs.Program))] + public void NoSpecialEntryPointPatternMainNoArgs() + { + using var _ = RemoteExecutor.Invoke(() => + { + var factory = HostFactoryResolver.ResolveServiceProviderFactory(typeof(NoSpecialEntryPointPatternMainNoArgs.Program).Assembly, s_WaitTimeout); + + Assert.NotNull(factory); + Assert.IsAssignableFrom(factory(Array.Empty())); + }); + } + + [ConditionalFact(nameof(RequirementsMet))] + public void TopLevelStatements() + { + using var _ = RemoteExecutor.Invoke(() => + { + var assembly = Assembly.Load("TopLevelStatements"); + var factory = HostFactoryResolver.ResolveServiceProviderFactory(assembly, s_WaitTimeout); + + Assert.NotNull(factory); + Assert.IsAssignableFrom(factory(Array.Empty())); + }); } } } diff --git a/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/Microsoft.Extensions.HostFactoryResolver.Tests.csproj b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/Microsoft.Extensions.HostFactoryResolver.Tests.csproj index 8f5acc14b7f9e7..d065e56ddfd095 100644 --- a/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/Microsoft.Extensions.HostFactoryResolver.Tests.csproj +++ b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/Microsoft.Extensions.HostFactoryResolver.Tests.csproj @@ -1,6 +1,7 @@ $(NetCoreAppCurrent);net461 + true @@ -20,5 +21,15 @@ + + + + + + + + + + diff --git a/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/MockHostTypes/HostBuilder.cs b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/MockHostTypes/HostBuilder.cs deleted file mode 100644 index 92b7f9b290fbb2..00000000000000 --- a/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/MockHostTypes/HostBuilder.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace MockHostTypes -{ - public class HostBuilder : IHostBuilder - { - public IHost Build() => new Host(); - } -} diff --git a/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/MockHostTypes/MockHostTypes.csproj b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/MockHostTypes/MockHostTypes.csproj index 88e03293e6ce4c..ed642b4209adba 100644 --- a/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/MockHostTypes/MockHostTypes.csproj +++ b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/MockHostTypes/MockHostTypes.csproj @@ -5,4 +5,8 @@ true + + + + diff --git a/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/NoSpecialEntryPointPattern/NoSpecialEntryPointPattern.csproj b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/NoSpecialEntryPointPattern/NoSpecialEntryPointPattern.csproj new file mode 100644 index 00000000000000..716b25a87281da --- /dev/null +++ b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/NoSpecialEntryPointPattern/NoSpecialEntryPointPattern.csproj @@ -0,0 +1,13 @@ + + + + $(NetCoreAppCurrent);net461 + true + Exe + + + + + + + diff --git a/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/NoSpecialEntryPointPattern/Program.cs b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/NoSpecialEntryPointPattern/Program.cs new file mode 100644 index 00000000000000..4dafef644a8c2e --- /dev/null +++ b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/NoSpecialEntryPointPattern/Program.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Hosting; + +namespace NoSpecialEntryPointPattern +{ + public class Program + { + public static void Main(string[] args) + { + var host = new HostBuilder().Build(); + } + } +} diff --git a/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/NoSpecialEntryPointPatternExits/NoSpecialEntryPointPatternExits.csproj b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/NoSpecialEntryPointPatternExits/NoSpecialEntryPointPatternExits.csproj new file mode 100644 index 00000000000000..716b25a87281da --- /dev/null +++ b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/NoSpecialEntryPointPatternExits/NoSpecialEntryPointPatternExits.csproj @@ -0,0 +1,13 @@ + + + + $(NetCoreAppCurrent);net461 + true + Exe + + + + + + + diff --git a/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/MockHostTypes/Host.cs b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/NoSpecialEntryPointPatternExits/Program.cs similarity index 53% rename from src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/MockHostTypes/Host.cs rename to src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/NoSpecialEntryPointPatternExits/Program.cs index 31e4aa91efb7e8..9176dae239533f 100644 --- a/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/MockHostTypes/Host.cs +++ b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/NoSpecialEntryPointPatternExits/Program.cs @@ -3,10 +3,13 @@ using System; -namespace MockHostTypes +namespace NoSpecialEntryPointPatternExits { - public class Host : IHost + public class Program { - public IServiceProvider Services { get; } = new ServiceProvider(); + public static void Main(string[] args) + { + + } } } diff --git a/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/NoSpecialEntryPointPatternHangs/NoSpecialEntryPointPatternHangs.csproj b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/NoSpecialEntryPointPatternHangs/NoSpecialEntryPointPatternHangs.csproj new file mode 100644 index 00000000000000..716b25a87281da --- /dev/null +++ b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/NoSpecialEntryPointPatternHangs/NoSpecialEntryPointPatternHangs.csproj @@ -0,0 +1,13 @@ + + + + $(NetCoreAppCurrent);net461 + true + Exe + + + + + + + diff --git a/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/MockHostTypes/IHost.cs b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/NoSpecialEntryPointPatternHangs/Program.cs similarity index 50% rename from src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/MockHostTypes/IHost.cs rename to src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/NoSpecialEntryPointPatternHangs/Program.cs index 410e44c2dc283a..e8183fce884337 100644 --- a/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/MockHostTypes/IHost.cs +++ b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/NoSpecialEntryPointPatternHangs/Program.cs @@ -3,10 +3,13 @@ using System; -namespace MockHostTypes +namespace NoSpecialEntryPointPatternHangs { - public interface IHost + public class Program { - IServiceProvider Services { get; } + public static void Main(string[] args) + { + Console.ReadLine(); + } } } diff --git a/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/NoSpecialEntryPointPatternMainNoArgs/NoSpecialEntryPointPatternMainNoArgs.csproj b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/NoSpecialEntryPointPatternMainNoArgs/NoSpecialEntryPointPatternMainNoArgs.csproj new file mode 100644 index 00000000000000..716b25a87281da --- /dev/null +++ b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/NoSpecialEntryPointPatternMainNoArgs/NoSpecialEntryPointPatternMainNoArgs.csproj @@ -0,0 +1,13 @@ + + + + $(NetCoreAppCurrent);net461 + true + Exe + + + + + + + diff --git a/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/NoSpecialEntryPointPatternMainNoArgs/Program.cs b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/NoSpecialEntryPointPatternMainNoArgs/Program.cs new file mode 100644 index 00000000000000..e304864cd82476 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/NoSpecialEntryPointPatternMainNoArgs/Program.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.Extensions.Hosting; + +namespace NoSpecialEntryPointPatternMainNoArgs +{ + public class Program + { + public static void Main() + { + var host = new HostBuilder().Build(); + } + } +} diff --git a/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/NoSpecialEntryPointPatternThrows/NoSpecialEntryPointPatternThrows.csproj b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/NoSpecialEntryPointPatternThrows/NoSpecialEntryPointPatternThrows.csproj new file mode 100644 index 00000000000000..716b25a87281da --- /dev/null +++ b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/NoSpecialEntryPointPatternThrows/NoSpecialEntryPointPatternThrows.csproj @@ -0,0 +1,13 @@ + + + + $(NetCoreAppCurrent);net461 + true + Exe + + + + + + + diff --git a/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/NoSpecialEntryPointPatternThrows/Program.cs b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/NoSpecialEntryPointPatternThrows/Program.cs new file mode 100644 index 00000000000000..f01cc36661a471 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/NoSpecialEntryPointPatternThrows/Program.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace NoSpecialEntryPointPatternThrows +{ + public class Program + { + public static void Main(string[] args) + { + throw new Exception("Main just throws"); + } + } +} diff --git a/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/MockHostTypes/IHostBuilder.cs b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/TopLevelStatements/Program.cs similarity index 58% rename from src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/MockHostTypes/IHostBuilder.cs rename to src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/TopLevelStatements/Program.cs index ef73665296d748..92f51e33ef543c 100644 --- a/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/MockHostTypes/IHostBuilder.cs +++ b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/TopLevelStatements/Program.cs @@ -1,10 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace MockHostTypes -{ - public interface IHostBuilder - { - IHost Build(); - } -} +using System; +using Microsoft.Extensions.Hosting; + +var host = new HostBuilder().Build(); \ No newline at end of file diff --git a/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/TopLevelStatements/TopLevelStatements.csproj b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/TopLevelStatements/TopLevelStatements.csproj new file mode 100644 index 00000000000000..716b25a87281da --- /dev/null +++ b/src/libraries/Microsoft.Extensions.HostFactoryResolver/tests/TopLevelStatements/TopLevelStatements.csproj @@ -0,0 +1,13 @@ + + + + $(NetCoreAppCurrent);net461 + true + Exe + + + + + + + diff --git a/src/libraries/Microsoft.Extensions.Hosting/src/HostBuilder.cs b/src/libraries/Microsoft.Extensions.Hosting/src/HostBuilder.cs index 1cc812f6f8f199..7ed38f701f6ae6 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/src/HostBuilder.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/src/HostBuilder.cs @@ -3,6 +3,8 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Reflection; using Microsoft.Extensions.Configuration; @@ -127,13 +129,40 @@ public IHost Build() } _hostBuilt = true; + // REVIEW: If we want to raise more events outside of these calls then we will need to + // stash this in a field. + using var diagnosticListener = new DiagnosticListener("Microsoft.Extensions.Hosting"); + const string hostBuildingEventName = "HostBuilding"; + const string hostBuiltEventName = "HostBuilt"; + + if (diagnosticListener.IsEnabled() && diagnosticListener.IsEnabled(hostBuildingEventName)) + { + Write(diagnosticListener, hostBuildingEventName, this); + } + BuildHostConfiguration(); CreateHostingEnvironment(); CreateHostBuilderContext(); BuildAppConfiguration(); CreateServiceProvider(); - return _appServices.GetRequiredService(); + var host = _appServices.GetRequiredService(); + if (diagnosticListener.IsEnabled() && diagnosticListener.IsEnabled(hostBuiltEventName)) + { + Write(diagnosticListener, hostBuiltEventName, host); + } + + return host; + } + + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:UnrecognizedReflectionPattern", + Justification = "The values being passed into Write are being consumed by the application already.")] + private static void Write( + DiagnosticSource diagnosticSource, + string name, + T value) + { + diagnosticSource.Write(name, value); } private void BuildHostConfiguration() diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/HostBuilderTests.cs b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/HostBuilderTests.cs index a07be287a3b95d..1f35e1c15f4802 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/HostBuilderTests.cs +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/HostBuilderTests.cs @@ -3,8 +3,10 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Reflection; +using Microsoft.DotNet.RemoteExecutor; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; @@ -32,6 +34,37 @@ public void DefaultConfigIsMutable() } } + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void BuildFiresEvents() + { + using var _ = RemoteExecutor.Invoke(() => + { + IHostBuilder hostBuilderFromEvent = null; + IHost hostFromEvent = null; + + var listener = new HostingListener((pair) => + { + if (pair.Key == "HostBuilding") + { + hostBuilderFromEvent = (IHostBuilder)pair.Value; + } + + if (pair.Key == "HostBuilt") + { + hostFromEvent = (IHost)pair.Value; + } + }); + + using var sub = DiagnosticListener.AllListeners.Subscribe(listener); + + var hostBuilder = new HostBuilder(); + var host = hostBuilder.Build(); + + Assert.Same(hostBuilder, hostBuilderFromEvent); + Assert.Same(host, hostFromEvent); + }); + } + [Fact] public void ConfigureHostConfigurationPropagated() { @@ -657,6 +690,32 @@ public void HostBuilderCanConfigureBackgroundServiceExceptionBehavior( options.Value.BackgroundServiceExceptionBehavior); } + private class HostingListener : IObserver, IObserver> + { + private IDisposable? _disposable; + private readonly Action> _callback; + + public HostingListener(Action> callback) + { + _callback = callback; + } + + public void OnCompleted() { _disposable?.Dispose(); } + public void OnError(Exception error) { } + public void OnNext(DiagnosticListener value) + { + if (value.Name == "Microsoft.Extensions.Hosting") + { + _disposable = value.Subscribe(this); + } + } + + public void OnNext(KeyValuePair value) + { + _callback(value); + } + } + private class FakeFileProvider : IFileProvider, IDisposable { public bool Disposed { get; private set; } diff --git a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Microsoft.Extensions.Hosting.Unit.Tests.csproj b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Microsoft.Extensions.Hosting.Unit.Tests.csproj index 8038a16a4d5afd..c5049661e96a16 100644 --- a/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Microsoft.Extensions.Hosting.Unit.Tests.csproj +++ b/src/libraries/Microsoft.Extensions.Hosting/tests/UnitTests/Microsoft.Extensions.Hosting.Unit.Tests.csproj @@ -3,6 +3,7 @@ $(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent);net461 true + true