Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ public Host(IServiceProvider services,

/// <summary>
/// Order:
/// IHostLifetime.WaitForStartAsync (can abort chain)
/// Services.GetService{IStartupValidator}().Validate() (can abort chain)
/// IHostLifetime.WaitForStartAsync
/// Services.GetService{IStartupValidator}().Validate()
/// IHostedLifecycleService.StartingAsync
/// IHostedService.Start
/// IHostedLifecycleService.StartedAsync
Expand Down Expand Up @@ -123,11 +123,11 @@ public async Task StartAsync(CancellationToken cancellationToken = default)
await ForeachService(_hostedLifecycleServices, token, concurrent, abortOnFirstException, exceptions,
(service, token) => service.StartingAsync(token)).ConfigureAwait(false);

// We do not abort on exceptions from StartingAsync.
// Exceptions in StartingAsync cause startup to be aborted.
LogAndRethrow();
}

// Call StartAsync().
// We do not abort on exceptions from StartAsync.
await ForeachService(_hostedServices, token, concurrent, abortOnFirstException, exceptions,
async (service, token) =>
{
Expand All @@ -139,14 +139,17 @@ await ForeachService(_hostedServices, token, concurrent, abortOnFirstException,
}
}).ConfigureAwait(false);

// Exceptions in StartAsync cause startup to be aborted.
LogAndRethrow();

// Call StartedAsync().
// We do not abort on exceptions from StartedAsync.
if (_hostedLifecycleServices is not null)
{
await ForeachService(_hostedLifecycleServices, token, concurrent, abortOnFirstException, exceptions,
(service, token) => service.StartedAsync(token)).ConfigureAwait(false);
}

// Exceptions in StartedAsync cause startup to be aborted.
LogAndRethrow();

// Call IHostApplicationLifetime.Started
Expand Down Expand Up @@ -329,7 +332,7 @@ private static async Task ForeachService<T>(
{
// The beginning synchronous portions of the implementations are run serially in registration order for
// performance since it is common to return Task.Completed as a noop.
// Any subsequent asynchronous portions are grouped together run concurrently.
// Any subsequent asynchronous portions are grouped together and run concurrently.
List<Task>? tasks = null;

foreach (T service in services)
Expand All @@ -354,6 +357,7 @@ private static async Task ForeachService<T>(
}
else
{
// The task encountered an await; add it to a list to run concurrently.
tasks ??= new();
tasks.Add(Task.Run(() => task, token));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,33 +313,82 @@ public async Task StartedAsync(CancellationToken cancellationToken)
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task StartPhasesException(bool throwAfterAsyncCall)
public async Task StartPhasesException_Starting(bool throwAfterAsyncCall)
{
ExceptionImpl impl = new(throwAfterAsyncCall: throwAfterAsyncCall, throwOnStartup: true, throwOnShutdown: false);
ExceptionImpl impl = new(throwAfterAsyncCall: throwAfterAsyncCall,
throwOnStarting: true, throwOnStart: false, throwOnStarted: false);

var hostBuilder = CreateHostBuilder(services =>
{
services.AddHostedService((token) => impl);
});

using (IHost host = hostBuilder.Build())
{
Exception ex = await Assert.ThrowsAnyAsync<Exception>(async () => await host.StartAsync());

Assert.True(impl.StartingCalled);
Assert.False(impl.StartCalled);
Assert.False(impl.StartedCalled);

Assert.Contains("(ThrowOnStarting)", ex.Message);
}
}

[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task StartPhasesException_Start(bool throwAfterAsyncCall)
{
ExceptionImpl impl = new(throwAfterAsyncCall: throwAfterAsyncCall,
throwOnStarting: false, throwOnStart: true, throwOnStarted: false);

var hostBuilder = CreateHostBuilder(services =>
{
services.AddHostedService((token) => impl);
});

using (IHost host = hostBuilder.Build())
{
Exception ex = await Assert.ThrowsAnyAsync<Exception>(async () => await host.StartAsync());

Assert.True(impl.StartingCalled);
Assert.True(impl.StartCalled);
Assert.False(impl.StartedCalled);

Assert.Contains("(ThrowOnStart)", ex.Message);
}
}

[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task StartPhasesException_Started(bool throwAfterAsyncCall)
{
ExceptionImpl impl = new(throwAfterAsyncCall: throwAfterAsyncCall,
throwOnStarting: false, throwOnStart: false, throwOnStarted: true);

var hostBuilder = CreateHostBuilder(services =>
{
services.AddHostedService((token) => impl);
});

using (IHost host = hostBuilder.Build())
{
AggregateException ex = await Assert.ThrowsAnyAsync<AggregateException>(async () => await host.StartAsync());
Exception ex = await Assert.ThrowsAnyAsync<Exception>(async () => await host.StartAsync());

Assert.True(impl.StartingCalled);
Assert.True(impl.StartCalled);
Assert.True(impl.StartedCalled);

Assert.Equal(3, ex.InnerExceptions.Count);
Assert.Contains("(ThrowOnStarting)", ex.InnerExceptions[0].Message);
Assert.Contains("(ThrowOnStart)", ex.InnerExceptions[1].Message);
Assert.Contains("(ThrowOnStarted)", ex.InnerExceptions[2].Message);
Assert.Contains("(ThrowOnStarted)", ex.Message);
}
}

[Fact]
public async Task ValidateOnStartAbortsChain()
{
ExceptionImpl impl = new(throwAfterAsyncCall: true, throwOnStartup: true, throwOnShutdown: false);
ExceptionImpl impl = new(throwAfterAsyncCall: false, throwOnStarting: false, throwOnStart: false, throwOnStarted: false);
var hostBuilder = CreateHostBuilder(services =>
{
services.AddHostedService((token) => impl)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ public async Task StoppedAsync(CancellationToken cancellationToken)
[InlineData(false)]
public async Task StopPhasesException(bool throwAfterAsyncCall)
{
ExceptionImpl impl = new(throwAfterAsyncCall: throwAfterAsyncCall, throwOnStartup: false, throwOnShutdown: true);
ExceptionImpl impl = new(throwAfterAsyncCall: throwAfterAsyncCall, throwOnShutdown: true);
var hostBuilder = CreateHostBuilder(services =>
{
services.AddHostedService((token) => impl);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,23 +180,35 @@ private class ExceptionImpl : IHostedLifecycleService
public bool StopCalled = false;
public bool StoppedCalled = false;

public bool ThrowOnStartup;
public bool ThrowOnStarting;
public bool ThrowOnStart;
public bool ThrowOnStarted;
public bool ThrowOnShutdown;

public ExceptionImpl(
bool throwAfterAsyncCall,
bool throwOnStartup,
bool throwOnShutdown)
{
_throwAfterAsyncCall = throwAfterAsyncCall;
ThrowOnStartup = throwOnStartup;
ThrowOnShutdown = throwOnShutdown;
}

public ExceptionImpl(
bool throwAfterAsyncCall,
bool throwOnStarting,
bool throwOnStart,
bool throwOnStarted)
{
_throwAfterAsyncCall = throwAfterAsyncCall;
ThrowOnStarting = throwOnStarting;
ThrowOnStart = throwOnStart;
ThrowOnStarted = throwOnStarted;
}

public async Task StartingAsync(CancellationToken cancellationToken)
{
StartingCalled = true;
if (ThrowOnStartup)
if (ThrowOnStarting)
{
if (_throwAfterAsyncCall)
{
Expand All @@ -210,7 +222,7 @@ public async Task StartingAsync(CancellationToken cancellationToken)
public async Task StartAsync(CancellationToken cancellationToken)
{
StartCalled = true;
if (ThrowOnStartup)
if (ThrowOnStart)
{
if (_throwAfterAsyncCall)
{
Expand All @@ -224,7 +236,7 @@ public async Task StartAsync(CancellationToken cancellationToken)
public async Task StartedAsync(CancellationToken cancellationToken)
{
StartedCalled = true;
if (ThrowOnStartup)
if (ThrowOnStarted)
{
if (_throwAfterAsyncCall)
{
Expand Down