Skip to content

Commit

Permalink
Run user defined hosted services before the server starts. (dotnet#36122
Browse files Browse the repository at this point in the history
)

* Run user defined hosted services before the server starts.
- The generic web hosted service is added before any user code runs, as a result its impossible to write a hosted services that run before server startup. This behavior differs from what happens when you use the Startup class today. We call ConfigureServices then we add the IHostedService implementation that starts the server. This change mimics that behavior by adding the initial set of hosted services after your code runs.
- Added a test
  • Loading branch information
davidfowl authored Sep 3, 2021
1 parent a46272f commit 45523fd
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 6 deletions.
17 changes: 17 additions & 0 deletions src/DefaultBuilder/src/WebApplicationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,15 @@ internal WebApplicationBuilder(WebApplicationOptions options, Action<IHostBuilde

Configuration = new();

// Collect the hosted services separately since we want those to run after the user's hosted services
_services.TrackHostedServices = true;

// This is the application configuration
var hostContext = _bootstrapHostBuilder.RunDefaultCallbacks(Configuration, _hostBuilder);

// Stop tracking here
_services.TrackHostedServices = false;

// Grab the WebHostBuilderContext from the property bag to use in the ConfigureWebHostBuilder
var webHostContext = (WebHostBuilderContext)hostContext.Properties[typeof(WebHostBuilderContext)];

Expand Down Expand Up @@ -155,6 +161,17 @@ public WebApplication Build()
services.Add(s);
}

// Add the hosted services that were initially added last
// this makes sure any hosted services that are added run after the initial set
// of hosted services. This means hosted services run before the web host starts.
foreach (var s in _services.HostedServices)
{
services.Add(s);
}

// Clear the hosted services list out
_services.HostedServices.Clear();

// Add any services to the user visible service collection so that they are observable
// just in case users capture the Services property. Orchard does this to get a "blueprint"
// of the service collection
Expand Down
19 changes: 13 additions & 6 deletions src/DefaultBuilder/src/WebApplicationServiceCollection.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
// 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.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace Microsoft.AspNetCore
{
internal sealed class WebApplicationServiceCollection : IServiceCollection
{
private IServiceCollection _services = new ServiceCollection();

public List<ServiceDescriptor> HostedServices { get; } = new();

public bool TrackHostedServices { get; set; }

public ServiceDescriptor this[int index]
{
get => _services[index];
Expand Down Expand Up @@ -42,7 +42,14 @@ public void Add(ServiceDescriptor item)
{
CheckServicesAccess();

_services.Add(item);
if (TrackHostedServices && item.ServiceType == typeof(IHostedService))
{
HostedServices.Add(item);
}
else
{
_services.Add(item);
}
}

public void Clear()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,67 @@ public async Task WebApplicationUrls_ThrowsInvalidOperationExceptionIfThereIsNoI
Assert.Throws<InvalidOperationException>(() => app.Urls);
}

[Fact]
public async Task HostedServicesRunBeforeTheServerStarts()
{
var builder = WebApplication.CreateBuilder();
var startOrder = new List<object>();
var server = new MockServer(startOrder);
var hostedService = new HostedService(startOrder);
builder.Services.AddSingleton<IHostedService>(hostedService);
builder.Services.AddSingleton<IServer>(server);
await using var app = builder.Build();

await app.StartAsync();

Assert.Equal(2, startOrder.Count);
Assert.Same(hostedService, startOrder[0]);
Assert.Same(server, startOrder[1]);
}

class HostedService : IHostedService
{
private readonly List<object> _startOrder;

public HostedService(List<object> startOrder)
{
_startOrder = startOrder;
}

public Task StartAsync(CancellationToken cancellationToken)
{
_startOrder.Add(this);
return Task.CompletedTask;
}

public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

class MockServer : IServer
{
private readonly List<object> _startOrder;

public MockServer(List<object> startOrder)
{
_startOrder = startOrder;
}

public IFeatureCollection Features { get; } = new FeatureCollection();

public void Dispose() { }

public Task StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken) where TContext : notnull
{
_startOrder.Add(this);
return Task.CompletedTask;
}

public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}

[Fact]
public async Task WebApplicationRunUrls_ThrowsInvalidOperationExceptionIfThereIsNoIServerAddressesFeature()
{
Expand Down

0 comments on commit 45523fd

Please sign in to comment.