{
public HostOptions() { }
public Microsoft.Extensions.Hosting.BackgroundServiceExceptionBehavior BackgroundServiceExceptionBehavior { get { throw null; } set { } }
+ public bool ServicesStartConcurrently { get { throw null; } set { } }
+ public bool ServicesStopConcurrently { get { throw null; } set { } }
public System.TimeSpan ShutdownTimeout { get { throw null; } set { } }
}
}
/// </summary>
public TimeSpan ShutdownTimeout { get; set; } = TimeSpan.FromSeconds(30);
+ /// <summary>
+ /// Determines if the <see cref="IHost"/> will start registered instances of <see cref="IHostedService"/> concurrently or sequentially. Defaults to false.
+ /// </summary>
+ public bool ServicesStartConcurrently { get; set; }
+
+ /// <summary>
+ /// Determines if the <see cref="IHost"/> will stop registered instances of <see cref="IHostedService"/> concurrently or sequentially. Defaults to false.
+ /// </summary>
+ public bool ServicesStopConcurrently { get; set; }
+
/// <summary>
/// The behavior the <see cref="IHost"/> will follow when any of
/// its <see cref="BackgroundService"/> instances throw an unhandled exception.
{
ShutdownTimeout = TimeSpan.FromSeconds(seconds);
}
+
+ var servicesStartConcurrently = configuration["servicesStartConcurrently"];
+ if (!string.IsNullOrEmpty(servicesStartConcurrently)
+ && bool.TryParse(servicesStartConcurrently, out bool startBehavior))
+ {
+ ServicesStartConcurrently = startBehavior;
+ }
+
+ var servicesStopConcurrently = configuration["servicesStopConcurrently"];
+ if (!string.IsNullOrEmpty(servicesStopConcurrently)
+ && bool.TryParse(servicesStopConcurrently, out bool stopBehavior))
+ {
+ ServicesStopConcurrently = stopBehavior;
+ }
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
combinedCancellationToken.ThrowIfCancellationRequested();
_hostedServices = Services.GetRequiredService<IEnumerable<IHostedService>>();
- foreach (IHostedService hostedService in _hostedServices)
+ List<Exception> exceptions = new List<Exception>();
+
+ if (_options.ServicesStartConcurrently)
{
- // Fire IHostedService.Start
- await hostedService.StartAsync(combinedCancellationToken).ConfigureAwait(false);
+ Task tasks = Task.WhenAll(_hostedServices.Select(async service =>
+ {
+ await service.StartAsync(combinedCancellationToken).ConfigureAwait(false);
+
+ if (service is BackgroundService backgroundService)
+ {
+ _ = TryExecuteBackgroundServiceAsync(backgroundService);
+ }
+ }));
- if (hostedService is BackgroundService backgroundService)
+ try
{
- _ = TryExecuteBackgroundServiceAsync(backgroundService);
+ await tasks.ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ exceptions.AddRange(tasks.Exception?.InnerExceptions ?? new[] { ex }.AsEnumerable());
+ }
+ }
+ else
+ {
+ foreach (IHostedService hostedService in _hostedServices)
+ {
+ try
+ {
+ // Fire IHostedService.Start
+ await hostedService.StartAsync(combinedCancellationToken).ConfigureAwait(false);
+
+ if (hostedService is BackgroundService backgroundService)
+ {
+ _ = TryExecuteBackgroundServiceAsync(backgroundService);
+ }
+ }
+ catch (Exception ex)
+ {
+ exceptions.Add(ex);
+ break;
+ }
+ }
+ }
+
+ if (exceptions.Count > 0)
+ {
+ if (exceptions.Count == 1)
+ {
+ // Rethrow if it's a single error
+ Exception singleException = exceptions[0];
+ _logger.HostedServiceStartupFaulted(singleException);
+ ExceptionDispatchInfo.Capture(singleException).Throw();
+ }
+ else
+ {
+ var ex = new AggregateException("One or more hosted services failed to start.", exceptions);
+ _logger.HostedServiceStartupFaulted(ex);
+ throw ex;
}
}
var exceptions = new List<Exception>();
if (_hostedServices != null) // Started?
{
- foreach (IHostedService hostedService in _hostedServices.Reverse())
+ // Ensure hosted services are stopped in LIFO order
+ IEnumerable<IHostedService> hostedServices = _hostedServices.Reverse();
+
+ if (_options.ServicesStopConcurrently)
{
+ Task tasks = Task.WhenAll(hostedServices.Select(async service => await service.StopAsync(token).ConfigureAwait(false)));
+
try
{
- await hostedService.StopAsync(token).ConfigureAwait(false);
+ await tasks.ConfigureAwait(false);
}
catch (Exception ex)
{
- exceptions.Add(ex);
+ exceptions.AddRange(tasks.Exception?.InnerExceptions ?? new[] { ex }.AsEnumerable());
+ }
+ }
+ else
+ {
+ foreach (IHostedService hostedService in hostedServices)
+ {
+ try
+ {
+ await hostedService.StopAsync(token).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ exceptions.Add(ex);
+ }
}
}
}
if (exceptions.Count > 0)
{
- var ex = new AggregateException("One or more hosted services failed to stop.", exceptions);
- _logger.StoppedWithException(ex);
- throw ex;
+ if (exceptions.Count == 1)
+ {
+ // Rethrow if it's a single error
+ Exception singleException = exceptions[0];
+ _logger.StoppedWithException(singleException);
+ ExceptionDispatchInfo.Capture(singleException).Throw();
+ }
+ else
+ {
+ var ex = new AggregateException("One or more hosted services failed to stop.", exceptions);
+ _logger.StoppedWithException(ex);
+ throw ex;
+ }
}
}
message: SR.BackgroundServiceExceptionStoppedHost);
}
}
+
+ public static void HostedServiceStartupFaulted(this ILogger logger, Exception? ex)
+ {
+ if (logger.IsEnabled(LogLevel.Error))
+ {
+ logger.LogError(
+ eventId: LoggerEventIds.HostedServiceStartupFaulted,
+ exception: ex,
+ message: "Hosting failed to start");
+ }
+ }
}
}
public static readonly EventId ApplicationStoppedException = new EventId(8, nameof(ApplicationStoppedException));
public static readonly EventId BackgroundServiceFaulted = new EventId(9, nameof(BackgroundServiceFaulted));
public static readonly EventId BackgroundServiceStoppingHost = new EventId(10, nameof(BackgroundServiceStoppingHost));
+ public static readonly EventId HostedServiceStartupFaulted = new EventId(11, nameof(HostedServiceStartupFaulted));
}
}
--- /dev/null
+// 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 System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.Extensions.Hosting.Unit.Tests;
+
+internal class DelegateHostedService : IHostedService, IDisposable
+{
+ private readonly Action _started;
+ private readonly Action _stopping;
+ private readonly Action _disposing;
+
+ public DelegateHostedService(Action started, Action stopping, Action disposing)
+ {
+ _started = started;
+ _stopping = stopping;
+ _disposing = disposing;
+ }
+
+ public Task StartAsync(CancellationToken token)
+ {
+ StartDate = DateTimeOffset.Now;
+ _started();
+ return Task.CompletedTask;
+ }
+ public Task StopAsync(CancellationToken token)
+ {
+ StopDate = DateTimeOffset.Now;
+ _stopping();
+ return Task.CompletedTask;
+ }
+
+ public void Dispose() => _disposing();
+
+ public DateTimeOffset StartDate { get; private set; }
+ public DateTimeOffset StopDate { get; private set; }
+}
using System.IO;
using System.Linq;
using System.Reflection;
-using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.DotNet.RemoteExecutor;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.UserSecrets;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting.Unit.Tests;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Xunit;
await host.StopAsync(cts.Token);
}
+ public static IEnumerable<object[]> StartAsync_StopAsync_Concurrency_TestCases
+ {
+ get
+ {
+ foreach (bool stopConcurrently in new[] { true, false })
+ {
+ foreach (bool startConcurrently in new[] { true, false })
+ {
+ foreach (int hostedServiceCount in new[] { 1, 4, 10 })
+ {
+ yield return new object[] { stopConcurrently, startConcurrently, hostedServiceCount };
+ }
+ }
+ }
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(StartAsync_StopAsync_Concurrency_TestCases))]
+ public async Task StartAsync_StopAsync_Concurrency(bool stopConcurrently, bool startConcurrently, int hostedServiceCount)
+ {
+ var hostedServices = new DelegateHostedService[hostedServiceCount];
+ bool[,] events = new bool[hostedServiceCount, 2];
+
+ for (int i = 0; i < hostedServiceCount; i++)
+ {
+ var index = i;
+ var service = new DelegateHostedService(() => { events[index, 0] = true; }, () => { events[index, 1] = true; } , () => { });
+
+ hostedServices[index] = service;
+ }
+
+ using var host = Host.CreateDefaultBuilder().ConfigureHostConfiguration(configBuilder =>
+ {
+ configBuilder.AddInMemoryCollection(new KeyValuePair<string, string>[]
+ {
+ new KeyValuePair<string, string>("servicesStartConcurrently", startConcurrently.ToString()),
+ new KeyValuePair<string, string>("servicesStopConcurrently", stopConcurrently.ToString())
+ });
+ }).ConfigureServices(serviceCollection =>
+ {
+ foreach (var hostedService in hostedServices)
+ {
+ serviceCollection.Add(ServiceDescriptor.Singleton<IHostedService>(hostedService));
+ }
+ }).Build();
+
+ await host.StartAsync(CancellationToken.None);
+
+ // Verifies that StartAsync had been called and that StopAsync had not been launched yet
+ for (int i = 0; i < hostedServiceCount; i++)
+ {
+ Assert.True(events[i, 0]);
+ Assert.False(events[i, 1]);
+ }
+
+ // Ensures that IHostedService instances are started in FIFO order
+ AssertExtensions.CollectionEqual(hostedServices, hostedServices.OrderBy(h => h.StartDate), EqualityComparer<DelegateHostedService>.Default);
+
+ await host.StopAsync(CancellationToken.None);
+
+ // Verifies that StopAsync had been called
+ for (int i = 0; i < hostedServiceCount; i++)
+ {
+ Assert.True(events[i, 1]);
+ }
+
+ // Ensures that IHostedService instances are stopped in LIFO order
+ AssertExtensions.CollectionEqual(hostedServices.Reverse(), hostedServices.OrderBy(h => h.StopDate), EqualityComparer<DelegateHostedService>.Default);
+ }
+
[Fact]
public void CreateDefaultBuilder_IncludesContentRootByDefault()
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
-using System.Diagnostics.Tracing;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting.Fakes;
using Microsoft.Extensions.Hosting.Tests;
using Microsoft.Extensions.Hosting.Tests.Fakes;
+using Microsoft.Extensions.Hosting.Unit.Tests;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;
}
}
- [Fact]
- public async Task AppCrashesOnStartWhenFirstHostedServiceThrows()
+ [Theory]
+ [InlineData(1, true), InlineData(1, false)]
+ [InlineData(2, true), InlineData(2, false)]
+ [InlineData(10, true), InlineData(10, false)]
+ public async Task AppCrashesOnStartWhenFirstHostedServiceThrows(int eventCount, bool concurrentStartup)
{
- bool[] events1 = null;
- bool[] events2 = null;
+ bool[][] events = new bool[eventCount][];
using (var host = CreateBuilder()
- .ConfigureServices((services) =>
+ .ConfigureServices(services =>
{
- events1 = RegisterCallbacksThatThrow(services);
- events2 = RegisterCallbacksThatThrow(services);
+ services.Configure<HostOptions>(i => i.ServicesStartConcurrently = concurrentStartup);
+
+ for (int i = 0; i < eventCount; i++)
+ {
+ events[i] = RegisterCallbacksThatThrow(services);
+ }
})
.Build())
{
- await Assert.ThrowsAsync<InvalidOperationException>(() => host.StartAsync());
- Assert.True(events1[0]);
- Assert.False(events2[0]);
+ if (concurrentStartup && eventCount > 1)
+ {
+ await Assert.ThrowsAsync<AggregateException>(() => host.StartAsync());
+ }
+ else
+ {
+ await Assert.ThrowsAsync<InvalidOperationException>(() => host.StartAsync());
+ }
+
+ for (int i = 0; i < eventCount; i++)
+ {
+ if (i == 0 || concurrentStartup)
+ {
+ Assert.True(events[i][0]);
+ }
+ else
+ {
+ Assert.False(events[i][0]);
+ }
+ }
+
host.Dispose();
+
// Stopping
- Assert.False(events1[1]);
- Assert.False(events2[1]);
+ for (int i = 0; i < eventCount; i++)
+ {
+ Assert.False(events[i][1]);
+ }
}
}
}
}
- [Fact]
- public async Task HostDoesNotNotifyIHostApplicationLifetimeCallbacksIfIHostedServicesThrow()
+ [Theory]
+ [InlineData(1, true), InlineData(1, false)]
+ [InlineData(2, true), InlineData(2, false)]
+ [InlineData(10, true), InlineData(10, false)]
+ public async Task HostDoesNotNotifyIHostApplicationLifetimeCallbacksIfIHostedServicesThrow(int eventCount, bool concurrentStartup)
{
- bool[] events1 = null;
- bool[] events2 = null;
+ bool[][] events = new bool[eventCount][];
using (var host = CreateBuilder()
.ConfigureServices((services) =>
{
- events1 = RegisterCallbacksThatThrow(services);
- events2 = RegisterCallbacksThatThrow(services);
+ services.Configure<HostOptions>(i => i.ServicesStartConcurrently = concurrentStartup);
+
+ for (int i = 0; i < eventCount; i++)
+ {
+ events[i] = RegisterCallbacksThatThrow(services);
+ }
})
.Build())
{
var applicationLifetime = host.Services.GetService<IHostApplicationLifetime>();
-
var started = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStarted);
var stopping = RegisterCallbacksThatThrow(applicationLifetime.ApplicationStopping);
- await Assert.ThrowsAsync<InvalidOperationException>(() => host.StartAsync());
- Assert.True(events1[0]);
- Assert.False(events2[0]);
+ if (concurrentStartup && eventCount > 1)
+ {
+ await Assert.ThrowsAsync<AggregateException>(() => host.StartAsync());
+ }
+ else
+ {
+ await Assert.ThrowsAsync<InvalidOperationException>(() => host.StartAsync());
+ }
+
+ for (int i = 0; i < eventCount; i++)
+ {
+ if (i == 0 || concurrentStartup)
+ {
+ Assert.True(events[i][0]);
+ }
+ else
+ {
+ Assert.False(events[i][0]);
+ }
+ }
+
Assert.False(started.All(s => s));
+
host.Dispose();
- Assert.False(events1[1]);
- Assert.False(events2[1]);
+
+ for (int i = 0; i < eventCount; i++)
+ {
+ Assert.False(events[i][1]);
+ }
+
Assert.False(stopping.All(s => s));
}
}
}
}
- private class DelegateHostedService : IHostedService, IDisposable
- {
- private readonly Action _started;
- private readonly Action _stopping;
- private readonly Action _disposing;
-
- public DelegateHostedService(Action started, Action stopping, Action disposing)
- {
- _started = started;
- _stopping = stopping;
- _disposing = disposing;
- }
-
- public Task StartAsync(CancellationToken token)
- {
- _started();
- return Task.CompletedTask;
- }
- public Task StopAsync(CancellationToken token)
- {
- _stopping();
- return Task.CompletedTask;
- }
-
- public void Dispose() => _disposing();
- }
-
private class AsyncDisposableService : IAsyncDisposable
{
public bool DisposeAsyncCalled { get; set; }