10

I've implemented IHostedService in an asp.net core website. It works great but the problem is that I want it to be started when the hosting server boots up or the IIS service restarts but it won't unless the first request to the website comes in.

  • The website is hosted on IIS version 10.0.18
  • The AppPool is in "AlwaysRunning" mode
  • "PreloadEnabled" is "True" on the website.
  • Setting ".NET CLR Version" to "No Managed Code" or "v4.0.xxxxxx" did not helped.
  • dotnet core version is 2.2 and dotnet core hosting bundle is installed.

UPDATE 1: "Application Initialization Module", Suggested by @Arthur did not help. Neither on site nor server level.

The configuration I used:

    <applicationInitialization
         doAppInitAfterRestart="true"
         skipManagedModules="false"
         remapManagedRequestsTo="init.htm">
        <add initializationPage="/init.htm" hostName="localhost"/>
    </applicationInitialization>

UPDATE 2: Here is how I implemented the interface

internal class PaymentQueueService : IHostedService, IDisposable
{
    private readonly ILogger _logService;
    private Timer _timerEnqueue;

    public PaymentQueueService(ILogger logService)
    {
        _logService = logService;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logService.LogInformation("Starting processing payments.");

        _timerEnqueue = new Timer(EnqueuePayments, null, TimeSpan.Zero,
            TimeSpan.FromSeconds(10));

        return Task.CompletedTask;
    }

    private void EnqueuePayments(object state)
    {
        _logService.LogInformation("Enqueueing Payments.");
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _logService.LogInformation("Stopping processing payments.");

        _timerEnqueue?.Change(Timeout.Infinite, 0);

        return Task.CompletedTask;
    }

    public void Dispose()
    {
        _timerEnqueue?.Dispose();
    }
}

The Program class in main.cs file:

public class Program
    {
        public static void Main(string[] args)
        {
            CreateWebHostBuilder(args).Build().Run();
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args).ConfigureServices(services =>
            {
                services.AddHostedService<PaymentQueueService>();
            }).Configure((IApplicationBuilder app) =>
            {

                app.UseMvc();
            })
                .UseStartup<Startup>();
    }

The Startup class:

public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {

        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostEnvironment env)
        {

        }
    }
Hossein
  • 1,640
  • 2
  • 26
  • 41
  • 1
    Maybe this will help [Application Initialization](https://learn.microsoft.com/en-us/iis/configuration/system.webserver/applicationinitialization/) – Arthur Nov 13 '19 at 08:20
  • @Arthur, It didn't help. I updated my question with your suggestion. – Hossein Nov 13 '19 at 13:37
  • Does it start when run on IIS-Express or Standalone (on your dev PC) ? – H H Nov 20 '19 at 07:21
  • How do you start/configure the service? Best to provide a [mcve] – H H Nov 20 '19 at 07:21
  • @HenkHolterman I added the implementation. – Hossein Nov 20 '19 at 08:03
  • @HenkHolterman It works on IIS-Express, because immediately after you start the app, it opens a browser and sends the first request to the website. – Hossein Nov 20 '19 at 08:07
  • You can tweak launchsettings.json, I think you can make it not open a browser. – H H Nov 20 '19 at 08:09
  • Any particular reason for not using the Startup class? – H H Nov 20 '19 at 08:12
  • @HenkHolterman I used Startup class at first, but then I moved my codes to the Main class. No particular reason and it does not make a difference. – Hossein Nov 20 '19 at 08:41
  • @HenkHolterman The main problem is getting it working on IIS. You think fixing it on IIS-Express would help? If yes, I will try that one too. – Hossein Nov 20 '19 at 08:43
  • It might help to diagnose this. But your config looks OK, it ought to start. – H H Nov 20 '19 at 08:46
  • @HenkHolterman May it be a issue or bug in IIS? – Hossein Nov 20 '19 at 10:17
  • Maybe, but it might be 'by-design' as well. I don't have time to look into it now. – H H Nov 20 '19 at 11:08
  • See also: [Recycle of app pool kills Kestrel but does not restart](https://stackoverflow.com/questions/47509674/recycle-of-app-pool-kills-kestrel-but-does-not-restart) - a workaround would be a 'keep alive' script that periodically hits the site. You could easily set this up via a scheduled task. Not pretty, but it could help. – SpruceMoose Nov 20 '19 at 15:16
  • @desmati create a HttpClient and call an endpoint to kick start the service. Try using async-await. Either way, once hosted on IIS it needs a request to be made in order for the process to start. – Nkosi Nov 22 '19 at 02:03
  • @desmati were you abe to make it work? – tvdias Nov 30 '19 at 11:04
  • 1
    After killing a days time with the same issue on IIS v10 and ASP.NET Core 5 I've found it. In addition to the Application Initialization you have to set the Application Pool to `Startmode=AlwaysRunning` and the Site to `Preload Enabled=true` (both to find in the Advanced Settings). Then you can even Host your site `InProcess` (so no need for an obsolete `OutOfProcess`). Also see https://stackoverflow.com/a/46573873/1443733 – Soko Apr 07 '21 at 10:31

3 Answers3

1

Since the suggested "Application Initialization Module" did not work, you could consider making the call yourself with a client since

The module starts the process for the ASP.NET Core app when the first request arrives and restarts the app if it shuts down or crashes.

public class Program {
    static Lazy<HttpClient> client = new Lazy<HttpClient>();
    public static async Task Main(string[] args) {
        var host = CreateWebHostBuilder(args).Start();//non blocking start
        using (host) {
            bool started = false;
            do {
                var response = await client.Value.GetAsync("site root");
                started = response.IsSuccessStatusCode;
                await Task.Delay(someDelayHere);
            } while (!started);
            
            host.WaitForShutdown();
        }
    }

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .ConfigureServices(services => {
                services.AddHostedService<PaymentQueueService>();
            })
            .Configure((IApplicationBuilder app) => {
                app.UseMvc();
            })
            .UseStartup<Startup>();
}

NOTE:

To prevent apps hosted out-of-process from timing out, use either of the following approaches:

  • Ping the app from an external service in order to keep it running.
  • If the app only hosts background services, avoid IIS hosting and use a Windows Service to host the ASP.NET Core app.
Community
  • 1
  • 1
Nkosi
  • 235,767
  • 35
  • 427
  • 472
0

Hosting it "OutOfProcess" does the trick, but bear in mind it's not the preferred way. I've created a repo on GitHub with the given code and you can check the change on the last commit.

Source: https://github.com/tvdias/SO58831661

For a deep explanation on the differences between InProcess and OutOfProcess I recommend reading this page: https://weblog.west-wind.com/posts/2019/Mar/16/ASPNET-Core-Hosting-on-IIS-with-ASPNET-Core-22.

tvdias
  • 821
  • 2
  • 10
  • 25
0

Here is how I create my webservices. In the Startup.cs I inject my IHostedServices. Container is a class from StructureMap. I have copied it from an existing project, so it does not fit 100% to your sample.

public class Program
{
    public static void Main(string[] args)
    {
        Config.Directories.EnsureDirectoryTree();

        var isService = !(Debugger.IsAttached || args.Contains("--console"));
        var webHostService = MyWebHostService.BuildWebHostService(args.Where(arg => arg != "--console").ToArray());

        if (isService)
        {
            ServiceBase.Run(webHostService);
        }
        else
        {
            webHostService.InitializeBackend();
            webHostService.Host.Run();
        }
    }
}

public class MyWebHostService : WebHostService
{
    public static readonly Logger Logger = LogManager.GetCurrentClassLogger();

    public IWebHost Host { get; }

    public static ZlsWebHostService BuildWebHostService(string[] args)
    {
        ConfigureNLog();

        Logger.Info("{0} (v{1}) starting...", Config.ServiceName, GetApplicationVersion());

        // restore config files
        Config.Files.EnsureRestored();

        var host = CreateWebHostBuilder(args).Build();
        return new ZlsWebHostService(host);
    }

    public static IWebHostBuilder CreateWebHostBuilder(string[] args)
    {
        var pathToExe = Assembly.GetExecutingAssembly().Location;
        var pathToContentRoot = Path.GetDirectoryName(pathToExe);

        return WebHost.CreateDefaultBuilder()
            .UseKestrel()
            .UseContentRoot(pathToContentRoot)
            .ConfigureAppConfiguration((context, config) =>
            {
                config.SetBasePath(Config.Directories.ActiveConfig);
                config.AddJsonFile(Config.Files.KestrelConfig.RelativePath, true, true);})
            .ConfigureLogging((hostingContext, logging) =>
            {
                logging.ClearProviders();
                logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
                logging.AddConsole();
                if(hostingContext.HostingEnvironment.IsDevelopment())
                    logging.AddDebug();
            })
            .UseNLog()
            .UseStartup<Startup>();
    }

    public MyWebHostService(IWebHost host) : base(host)
    {
        this.Host = host;
    }

    protected override void OnStarting(string[] args)
    {
        InitializeBackend();

        base.OnStarting(args);
    }

    protected override void OnStarted()
    {
        Logger.Info("{0} started...", Config.ServiceName);
        base.OnStarted();
    }

    protected override void OnStopped()
    {
        Logger.Info("{0} stopped...", Config.ServiceName);
        base.OnStopped();
    }

...

}

public class Startup
{
...

    // This method gets called by the runtime. Use this method to add services to the container.
    // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
    public IServiceProvider ConfigureServices(IServiceCollection services)
    {
        ...

        services.AddSingleton<PaymentQueueService>();

        ...

        var container = new Container(c =>
        {
            c.IncludeRegistry<MyFooRegistry>();
            c.Populate(services);
        });
        return container.GetInstance<IServiceProvider>();
    }

...

}
Dominic Jonas
  • 4,717
  • 1
  • 34
  • 77