3

I'm trying to create a console application that run ahosted service with Simple Injector and I looked to the example at generichostintegration.

Now I would like to change IProcessor.DoSomeWork to be an async function with a cancellation token as parameter so DoSomeWork can be canceled:

public async void DoSomeWork(CancellationToken cancellationToken)
{
    await Task.Delay(_settings.Delay, cancellationToken);
}

How can i inject the cancellation token from HostedService

private void DoWork()
{
    try
    {
        using (AsyncScopedLifestyle.BeginScope(this.container))
        {
            var service = this.container.GetInstance<TService>();
            this.settings.Action(service);
        }
    }
    catch (Exception ex)
    {
        this.logger.LogError(ex, ex.Message);
    }
}

and configurate in the right way the container

container.RegisterInstance(new TimedHostedService<IProcessor>.Settings(
    interval: TimeSpan.FromSeconds(10),
    action: processor => processor.DoSomeWork()));

I'm a bit stack with that. Maybe i'm thinking wrong?

*** Updated ***

This is what I did in the end. I kept it as simple as possible.

class Program
{
    public static async Task Main(string[] args)
    {
        var container = new Container();

        IHost host = CreateHostBuilder(args, container)
                .Build()
                .UseSimpleInjector(container);

        ConfigureContainer(container);

        await host.RunAsync();
    }

    private static void ConfigureContainer(Container container)
    {
        container.Register<IWorkScheduler, WorkScheduler>(Lifestyle.Singleton);
        // Sets the schedule timer interval
        container.RegisterInstance(new WorkSchedulerSettings(TimeSpan.FromSeconds(1)));

        container.Register<DoSomethingWorker>();
        container.RegisterInstance(new DoSomethingSettings(new TimeSpan(0, 0, 5)));
        container.Register<DoSomethingElseWorker>();
        container.RegisterInstance(new DoSomethingElseSettings(new TimeSpan(0, 0, 10)));

        container.Verify();
    }

    public static IHostBuilder CreateHostBuilder(string[] args, Container container) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureServices((hostContext, services) =>
            {
                services.AddLogging();
                services.AddSimpleInjector(container, options =>
                {
                    // Registers the hosted service as singleton in Simple Injector
                    // and hooks it onto the .NET Core Generic Host pipeline.
                    options.AddHostedService<BackgroundHostedService>();
                    services.AddLogging();
                });
            })
        .UseConsoleLifetime();
}

the hosted service

public class BackgroundHostedService
    : BackgroundService
{
    private readonly IWorkScheduler _scheduler;
    private readonly Container _container;
    private readonly ILogger _logger;

    public BackgroundHostedService(IWorkScheduler scheduler, Container container, ILogger<BackgroundHostedService> logger)
    {
        _scheduler = scheduler ?? throw new ArgumentNullException(nameof(scheduler));
        _container = container ?? throw new ArgumentNullException(nameof(container));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public override Task StartAsync(CancellationToken cancellationToken)
    {
        LoadWorkers();
        return base.StartAsync(cancellationToken);
    }

    protected override Task ExecuteAsync(CancellationToken cancellationToken)
    {
        try
        {
            _scheduler.Start();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, ex.Message);
        }
        return Task.CompletedTask;
    }

    public override async Task StopAsync(CancellationToken cancellationToken)
    {
        await _scheduler.Stop();
    }

    private void LoadWorkers()
    {
        // Hook up triggers and specify span period and if they have to run once at time.
        WorkTrigger trigger1 = new WorkTrigger(_container.GetInstance<DoSomethingWorker>(), new TimeSpan(0, 0, 2), false);
        _scheduler.AddTrigger(trigger1);

        WorkTrigger trigger2 = new WorkTrigger(_container.GetInstance<DoSomethingElseWorker>(), new TimeSpan(0, 0, 5), true);
        _scheduler.AddTrigger(trigger2);
    }

    public override void Dispose()
    {
        _scheduler.Dispose();
        base.Dispose();
    }

the "pseudo" scheduler

public interface IWorkScheduler : IDisposable
{
    void Start();
    Task Stop();
    void AddTrigger(WorkTrigger trigger);

}

public class WorkSchedulerSettings
{
    public readonly TimeSpan Interval;

    public WorkSchedulerSettings(TimeSpan interval)
    {
        Interval = interval;
    }
}

public class WorkScheduler
    : IWorkScheduler, IDisposable
{
    private readonly Timer _timer;
    private readonly WorkSchedulerSettings _settings;
    private readonly ILogger<WorkScheduler> _logger;
    private readonly List<Task> _tasks;
    private readonly List<WorkTrigger> _triggers;
    private readonly CancellationTokenSource _cancTokenSource;

    public WorkScheduler(WorkSchedulerSettings settings, ILogger<WorkScheduler> logger)
    {
        _settings = settings ?? throw new ArgumentNullException(nameof(settings));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        _timer = new Timer(callback: _ => DoWork());
        _tasks = new List<Task>();
        _triggers = new List<WorkTrigger>();
        _cancTokenSource = new CancellationTokenSource();
    }

    public void Start()
    {
        _logger.LogInformation("Scheduler started");
        _timer.Change(dueTime: TimeSpan.Zero, period: _settings.Interval);
    }

    public async Task Stop()
    {
        _timer.Change(Timeout.Infinite, Timeout.Infinite);
        _cancTokenSource.Cancel();
        await Task.WhenAll(_tasks);
        _tasks.Clear();
        _logger.LogInformation("Scheduler stopped");
    }

    public void AddTrigger(WorkTrigger trigger)
    {
        if (trigger == null) throw new ArgumentNullException(nameof(trigger));
        _triggers.Add(trigger);
    }

    private void DoWork()
    {
        foreach (var trigger in _triggers)
        {
            if (trigger.CanExecute(DateTime.Now))
            {
                var task = trigger
                            .Execute(_cancTokenSource.Token)
                            .ContinueWith(x => HandleError(x));
                _tasks.Add(task);
            }
        }
        _tasks.RemoveAll(x => x.IsCompleted);
    }

    private void HandleError(Task task)
    {
        if (task.IsFaulted)
            _logger.LogError(task.Exception.Message);
    }

    public void Dispose()
    {
        _timer?.Dispose();
        _cancTokenSource?.Dispose();
    }
}
Paolo
  • 31
  • 2
  • 1
    You can store the `cancellationToken` in a private field in the `TimedHostedService`. This way `DoWork` has access to the cancellation token. Running `DoWork` asynchonously with `Timer` is, AFAIK, not something that is easily done, so you might be forced to call `GetAwaiter().GetResult()` on your awaited calls, but perhaps Google has some better answers here. – Steven Aug 28 '21 at 16:11
  • 1
    But perhaps it's good to describe what it is you are trying to achieve. Do you want to be able to cancel the startup process, or do you want to be able to stop your running hosted service? If it is the latter one you need, you probably need an internal cancellation token that gets cancelled by the `Stop` method of your hosted service. – Steven Aug 29 '21 at 09:11
  • Thank you Steven. I always read your wise answers and your book is one om my favorit :) Of course you are right, maybe i started the wrong way by looking at an example and trying to fit into my goal. But of course I have not yet understood all the mechanisms. What i want to archive is to create a windows service that runs different long-running tasks. And if the hosted service receives the stop signal, all tasks must be canceled and it waits until they finish. – Paolo Aug 29 '21 at 10:48
  • So i will try with one BackgroundService that start a really simple task scheduler that control if it is the time to start a task and avoid to lunch the same again if the previous didn't finish yet. If the hosted service receives the stop signal it will propagate that to the scheduler and the scheduler to the long-running tasks. So simple :) – Paolo Aug 29 '21 at 10:48
  • 1
    After you designed and tested your solution, it might be good to post it as an answer to your own question. – Steven Aug 29 '21 at 10:50
  • I tried following Steven's advice that is add an answer to my own question men it was deleted because "message is posted as an answer, which is wrong". So i changed my question and added the first part which is also the most interesting. – Paolo Sep 08 '21 at 06:21

0 Answers0