.NET Core 3.0 IHostedService access web server URL - scheme, host, port etc

Viewed 2065

The issue that I have is simple. I want to access the server URL in my IHostedService.

I cannot find a way to do it. There isn't any request there so I cannot use the IHttpContextAccessor.

IServer features property doesn't have any addresses and I am out of options.

I do not want to hard code the URL of the server in the configuration.

The version of .NET core that I am running is 3.0.

4 Answers

You can register your hosted service with the dependency injection framework in Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IHostedService, MyHostedService>();
}

Then, you can inject an IServer into your hosted service and get the addresses using IServerAddressesFeature:

public class MyHostedService : IHostedService
{
    private readonly IServer _server;

    public MyHostedService(IServer server)
    {
       _server = server;
    }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
       var features = _server.Features;
       var addresses = features.Get<IServerAddressesFeature>();
       var address = addresses.Addresses.FirstOrDefault(); // = http://localhost:5000
    }
}

You cannot get the address/url from your server before you completely start the server.

If you need your localhost address, look it up (programmatically or otherwise) from ./Properties/launchSettings.json.

If you need the address in a live environment, you have to wait for the server to load.

For Asp.Net Core 3.1, you cannot get addresses from IServerAddressesFeature in IHostedService because IHostedServices are launched before Server. See this issue.

What has changed is the ordering of IHostedService's being called. This was moved up in 3.0, but still runs after Configure.

To get addresses in your IHostedServices you can make them run After Startup. I found one of the approaches is to register your service in Program.cs, see it in detail here.

public class Program
{
    public static void Main(string[] args)
        => CreateHostBuilder(args).Build().Run();

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder => // The GenericWebHostSevice is registered here
            {
                webBuilder.UseStartup<Startup>();
            })
            // Register your HostedService AFTER ConfigureWebHostDefaults
            .ConfigureServices(
                services => services.AddHostedService<ProgramHostedService>());
}

In .NET 6, I couldn't find a way to affect the order in which the hosted services are started, so I had to find another way. Luckily, there's IHostApplicationLifetime which lets you hook into the ApplicationStarted lifecycle event. Oddly, ApplicationStarted is a CancellationToken, and not a C# event. You can use the Register method on the cancellation token to run code when the web application has started. At this point, the URLs will be populated on the IServer object.

Here's an example with IHostedService:

using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;

public class MyHostedService : IHostedService
{
    private readonly IServer _server;
    private readonly IHostApplicationLifetime _hostApplicationLifetime;

    public MyHostedService(IServer server, IHostApplicationLifetime hostApplicationLifetime)
    {
        _server = server;
        _hostApplicationLifetime = hostApplicationLifetime;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        Console.WriteLine($"Address before application has started: {GetAddress()}");

        _hostApplicationLifetime.ApplicationStarted.Register(
                () => Console.WriteLine($"Address after application has started: {GetAddress()}"));
        
        return Task.CompletedTask;
    }

    private string GetAddress()
    {
        var features = _server.Features;
        var addresses = features.Get<IServerAddressesFeature>();
        var address = addresses.Addresses.FirstOrDefault();
        return address;
    }

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

When you run the application, the output will look like this:

Address before application has started:
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: https://localhost:7012
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5012
Address after application has started: https://localhost:7012
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: C:\Users\niels\RiderProjects\UrlsInHostedService\UrlsInHostedService\

As you can see by the output, there's no address on IServer when the hosted service is started, but then the server starts, and the ApplicationStarted cancellation token is cancelled, which triggers the callback and now there are URLs on IServer.

In my case, I actually needed it for BackgroundService in which case it works better IMO, because you can create your own task and await it. Here's an example:

using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;

public class MyBackgroundService : BackgroundService
{
    private readonly IServer _server;
    private readonly IHostApplicationLifetime _hostApplicationLifetime;

    public MyBackgroundService(IServer server, IHostApplicationLifetime hostApplicationLifetime)
    {
        _server = server;
        _hostApplicationLifetime = hostApplicationLifetime;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        Console.WriteLine($"Address before application has started: {GetAddress()}");

        await WaitForApplicationStarted();
        
        Console.WriteLine($"Address after application has started: {GetAddress()}");
    }

    private string GetAddress()
    {
        var features = _server.Features;
        var addresses = features.Get<IServerAddressesFeature>();
        var address = addresses.Addresses.FirstOrDefault();
        return address;
    }    
    
    private Task WaitForApplicationStarted()
    {
        var completionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        _hostApplicationLifetime.ApplicationStarted.Register(() => completionSource.TrySetResult());
        return completionSource.Task;
    }
}

The result would be the same as the hosted service, but you do have to use BackgroundService and ExecuteAsync which may not make sense for your use case.

Related