dotnet / docs

This repository contains .NET Documentation.
https://learn.microsoft.com/dotnet
Creative Commons Attribution 4.0 International
4.23k stars 5.87k forks source link

Multuple IHostedService registration #12544

Closed igor-moskvitin closed 4 years ago

igor-moskvitin commented 5 years ago

If i try to register two or more services, only one could work properly. For example:

services.AddSingleton<IHostedService, ServiceA>();
services.AddSingleton<IHostedService, ServiceB>();

Implementations are simplest as possible:

public class ServiceA: IHostedService
    {
        public Task StartAsync(CancellationToken cancellationToken)
        {
            DoWork();
            return Task.CompletedTask;
        }

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

        private void DoWork()
        {
            while (true)
            {
                Console.WriteLine("ServiceA");
                Thread.Sleep(2000);
            }
        }
    }

and

public class ServiceB: IHostedService
    {
        public Task StartAsync(CancellationToken cancellationToken)
        {
            DoWork();
            return Task.CompletedTask;
        }

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

        private void DoWork()
        {
            while (true)
            {
                Console.WriteLine("ServiceB");
                Thread.Sleep(1000);
            }
        }
    }

In output getting messages only from ServiceA


Document Details

Do not edit this section. It is required for docs.microsoft.com ➟ GitHub issue linking.

yuexuanwo commented 5 years ago

The method DoWork() should be call like: await Task.Run(this.DoWork);

igor-moskvitin commented 5 years ago

@yuexuanwo, it doesnt matter how i call DoWork()

public async Task StartAsync(CancellationToken cancellationToken)
{
            await Task.Run(DoWork);
}

or

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

in output i can see only messages from first registerd service: like "ServiceA" if

services.AddSingleton<IHostedService, ServiceA>();
services.AddSingleton<IHostedService, ServiceB>();

and "ServiceB" if

services.AddSingleton<IHostedService, ServiceB>();
services.AddSingleton<IHostedService, ServiceA>();
xygon commented 5 years ago

StartAsync needs to return and neither DoWork() nor await Task.Run(DoWork) allow StartAsync to complete. Task.Run(DoWork) without await will work.

mvelosop commented 4 years ago

Hi @igor-moskvitin, @xygon, Thanks for your feedback.

It's just a little bit more involved.

To properly handle graceful cancellation you have to implement your services using the pattern shown in the BackgroundService class in the article.

Using the above pattern, DoWork is renamed to ExecuteAsync, but it's just the same!

However, since almost all of the code in BackgroundService is boilerplate, you'd be better off just inheriting your services from BackgroundService.

This way the final code gets pretty simple:

    public class Program
    {
        private static async Task Main(string[] args)
        {
            Console.WriteLine("Hosted services demo");
            Console.WriteLine();

            var builder = new HostBuilder()
                .ConfigureServices(services =>
                {
                    services.AddSingleton<IHostedService, ServiceA>();
                    services.AddSingleton<IHostedService, ServiceB>();
                });

            await builder.RunConsoleAsync();
        }
    }

    public class ServiceA : BackgroundService
    {
        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            stoppingToken.Register(() => Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} - Stopping Service A..."));

            while (!stoppingToken.IsCancellationRequested)
            {
                Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} - Service A running (thread {Thread.CurrentThread.ManagedThreadId})...");
                await Task.Delay(2200, stoppingToken);
            }
        }
    }

    public class ServiceB : BackgroundService
    {
        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            stoppingToken.Register(() => Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} - Stopping Service B..."));

            while (!stoppingToken.IsCancellationRequested)
            {
                Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} - Service B running (thread {Thread.CurrentThread.ManagedThreadId})...");
                await Task.Delay(1500, stoppingToken);
            }
        }
    }

And you get the output you expect:

11:00:07.149 - Service A running (thread 1)...
11:00:07.173 - Service B running (thread 1)...
Application started. Press Ctrl+C to shut down.
Hosting environment: Production
Content root path: C:\Users\Miguel\source\repos\explore-hosted-services\HostedServices\bin\Debug\netcoreapp2.2\
11:00:08.692 - Service B running (thread 4)...
11:00:09.379 - Service A running (thread 5)...
11:00:10.192 - Service B running (thread 4)...
11:00:11.595 - Service A running (thread 4)...
11:00:11.692 - Service B running (thread 5)...
11:00:14.539 - Service B running (thread 6)...
11:00:15.241 - Service A running (thread 6)...
11:00:15.983 - Stopping Service B...
11:00:16.021 - Stopping Service A...

In the above example the stopping was started with Ctrl+C.

Hope this helps.

igor-moskvitin commented 4 years ago

@mvelosop , thanks a lot!