简体   繁体   中英

Short running background task in .NET Core

I just discovered IHostedService and .NET Core 2.1 BackgroundService class. I think idea is awesome. Documentation .

All examples I found are used for long running tasks (until application die). But I need it for short time. Which is the correct way of doing it?

For example:
I want to execute a few queries (they will take approx. 10 seconds) after application starts. And only if in development mode. I do not want to delay application startup so IHostedService seems good approach. I can not use Task.Factory.StartNew , because I need dependency injection.

Currently I am doing like this:

public class UpdateTranslatesBackgroundService: BackgroundService
{
    private readonly MyService _service;

    public UpdateTranslatesBackgroundService(MyService service)
    {
        //MService injects DbContext, IConfiguration, IMemoryCache, ...
        this._service = service;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await ...
    }
}

startup:

public static IServiceProvider Build(IServiceCollection services, ...)
{
    //.....
    if (hostingEnvironment.IsDevelopment())
        services.AddSingleton<IHostedService, UpdateTranslatesBackgroundService>();
    //.....
}

But this seems overkill. Is it? Register singleton (that means class exists while application lives). I don't need this. Just create class, run method, dispose class. All in background task.

Well I think there is more then one question here. First let me point out something you are probably aware of async != multithreaded. So BackgroundService will not make you app "multithreaded" it can run inside a single thread without no problem. And if you are doing blocking operations on that thread it will still block startup. Lets say in the class you implement all the sql queries in a not real async way something similar to

public class StopStartupService : BackgroundService
    {
        protected override Task ExecuteAsync(CancellationToken stoppingToken)
        {
            System.Threading.Thread.Sleep(1000);
            return Task.CompletedTask;
        }
    }

This will still block startup.

So there is another question.

How should you run background jobs?

For this in simple cases Task.Run (Try to avoid Task.Factory.StartNew if you are not sure how to configure it) should do the job, but that is not to say this is the best or a good way to do it. There are a bunch of open source libraries that will do this for you and it might be good to have a look at what they provide. There are a lot of problems you might not be aware of , that can create frustrating bugs if you just use Task.Run The second question I can see is.

Should I do fire and forget in c#?

For me this is a definite NO(but XAML people might not agree). No matter what you do, you need to keep track of when the thing you are doing is done. In your case you might want to do a rollback in the database if someone stops the app before the queries are done. But more than that you would want to know when you can start using the data that the queries provided. So BackgroundService helps you to simplify the execution but is difficult to keep track of completion.

Should you use a singleton?

As you already mentioned using singletons can be a dangerous thing especially if you don't clean things properly, but more than that the context of the service you are using will be the same for the life time of the object. So with this all depends on your implementation of the service if there will be problems.

I do something like this to do what you want.

   public interface IStartupJob
    {
        Task ExecuteAsync(CancellationToken stoppingToken);
    }

    public class DBJob : IStartupJob
    {
        public Task ExecuteAsync(CancellationToken stoppingToken)
        {
            return Task.Run(() => System.Threading.Thread.Sleep(10000));
        }
    }

    public class StartupJobService<TJob> : IHostedService, IDisposable where TJob: class,IStartupJob
    {
        //This ensures a single start of the task this is important on a singletone
        private readonly Lazy<Task> _executingTask;

        private readonly CancellationTokenSource _stoppingCts = new CancellationTokenSource();

        public StartupJobService(Func<TJob> factory)
        {
            //In order for the transient item to be in memory as long as it is needed not to be in memory for the lifetime of the singleton I use a simple factory
            _executingTask = new Lazy<Task>(() => factory().ExecuteAsync(_stoppingCts.Token));
        }

        //You can use this to tell if the job is done
        public virtual Task Done => _executingTask.IsValueCreated ? _executingTask.Value : throw new Exception("BackgroundService not started");

        public virtual Task StartAsync(CancellationToken cancellationToken)
        {

            if (_executingTask.Value.IsCompleted)
            {
                return _executingTask.Value;
            }

            return Task.CompletedTask;
        }

        public virtual async Task StopAsync(CancellationToken cancellationToken)
        {
            if (_executingTask == null)
            {
                return;
            }

            try
            {
                _stoppingCts.Cancel();
            }
            finally
            {
                await Task.WhenAny(_executingTask.Value, Task.Delay(Timeout.Infinite,
                                                              cancellationToken));
            }

        }

        public virtual void Dispose()
        {
            _stoppingCts.Cancel();
        }

        public static void AddService(IServiceCollection services)
        {
            //Helper to register the job
            services.AddTransient<TJob, TJob>();

            services.AddSingleton<Func<TJob>>(cont => 
            {
                return () => cont.GetService<TJob>();
            });

            services.AddSingleton<IHostedService, StartupJobService<TJob>>();
        }
    }

There's no need to do any magic for this to work.

Simply:

  • Register the service you need to run in ConfigureServices
  • Resolve the instance you need in Configure and run it.
  • To avoid blocking, use Task.Run .

You must register the instance, or dependency injection won't work. That's unavoidable; if you need DI, then you have to do it.

Beyond that, it's trivial to do what you ask, like this:

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)
  {
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    services.AddTransient<MyTasks>(); // <--- This
  }

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

      // Blocking
      app.ApplicationServices.GetRequiredService<MyTasks>().Execute();

      // Non-blocking
      Task.Run(() => { app.ApplicationServices.GetRequiredService<MyTasks>().Execute(); });
    }
    else
    {
      app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseMvc();
  }
}

public class MyTasks
{
  private readonly ILogger _logger;

  public MyTasks(ILogger<MyTasks> logger)
  {
    _logger = logger;
  }

  public void Execute()
  {
    _logger.LogInformation("Hello World");
  }
}

BackgroundService exists specifically for long running processes; if it's a once of, don't use it.

There is a library called Communist.StartupTasks that handles this exact scenario. It's available on Nuget.

It's designed specifically to run tasks during application launch in a .NET Core App. It fully supports dependency injection.

Please note that it executes tasks sequentially and it blocks until all tasks are complete (ie your app won't accept requests until startup tasks complete).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM