web-dev-qa-db-fra.com

Courte tâche d'arrière-plan en cours d'exécution dans .NET Core

Je viens de découvrir IHostedService et .NET Core 2.1 BackgroundService class. Je pense que l'idée est géniale. Documentation .

Tous les exemples que j'ai trouvés sont utilisés pour des tâches de longue durée (jusqu'à la mort de l'application) . Mais j'en ai besoin pour une courte période. Quelle est la bonne façon de le faire?

Par exemple:
Je souhaite exécuter quelques requêtes (elles prendront environ 10 secondes) après le démarrage de l'application. Et seulement si en mode de développement. Je ne veux pas retarder le démarrage de l'application, alors IHostedService semble être une bonne approche. Je ne peux pas utiliser Task.Factory.StartNew, car j'ai besoin d'une injection de dépendance.

Actuellement je fais comme ça:

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 ...
    }
}

commencez:

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

Mais cela semble exagéré. Est ce Register singleton (cela signifie que la classe existe tant que l'application est en vie). Je n'ai pas besoin de ça. Il suffit de créer une classe, exécuter la méthode, disposer de la classe. Tout en tâche de fond.

8
Makla

Il n'y a pas besoin de faire de magie pour que cela fonctionne.

Simplement:

  • Enregistrez le service dont vous avez besoin pour exécuter ConfigureServices
  • Résolvez l'instance dont vous avez besoin dans Configure et exécutez-la.
  • Pour éviter le blocage, utilisez Task.Run.

Vous devez enregistrer l'instance, sinon l'injection de dépendance ne fonctionnera pas. C'est inévitable. si vous avez besoin de DI, vous devez le faire.

Au-delà de cela, il est trivial de faire ce que vous demandez, comme ceci:

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 existe spécifiquement pour les processus de longue durée; si c'est une fois, ne l'utilisez pas.

2
Doug

Eh bien, je pense qu'il y a plus d'une question ici. Tout d’abord, permettez-moi de signaler quelque chose que vous connaissez probablement asynchrone! = Multithread . Donc BackgroundService ne fera pas de votre application "multithread", elle peut s’exécuter dans un seul thread sans problème. Et si vous effectuez des opérations de blocage sur ce fil, le démarrage sera toujours bloqué. Disons que dans la classe, vous implémentez toutes les requêtes SQL de manière asynchrone, quelque chose de similaire à 

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

Cela bloquera toujours le démarrage.

Donc, il y a une autre question. 

Comment devriez-vous exécuter des travaux en arrière-plan?  

Pour cela, dans des cas simples, Task.Run (essayez d'éviter Task.Factory.StartNew si vous ne savez pas comment le configurer) devrait faire l'affaire, mais cela ne veut pas dire que c'est le meilleur ou le bon moyen de le faire. De nombreuses bibliothèques à code source libre le feront pour vous et il serait peut-être bon de regarder ce qu’elles fournissent. Il y a beaucoup de problèmes que vous ignorez peut-être, qui peuvent créer des bugs frustrants si vous utilisez simplement Task.Run La deuxième question que je peux voir est.

Devrais-je faire feu et oublier en c #?  

Pour moi, c’est un non catégorique (mais les gens de XAML pourraient ne pas être d’accord). Quoi que vous fassiez, vous devez savoir quand votre travail est terminé. Dans votre cas, vous souhaiterez peut-être effectuer une restauration de la base de données si quelqu'un arrête l'application avant que les requêtes ne soient terminées. Mais plus que cela, vous voudriez savoir quand vous pouvez commencer à utiliser les données fournies par les requêtes. Donc BackgroundService vous aide à simplifier l'exécution mais il est difficile de garder trace de son achèvement. 

Si vous utilisez un singleton?  

Comme vous l'avez déjà mentionné, l'utilisation de singletons peut s'avérer dangereuse, surtout si vous ne nettoyez pas les choses correctement, mais le contexte du service que vous utilisez sera le même pour toute la durée de vie de l'objet. Donc, avec cela, tout dépend de la mise en œuvre du service s’il y aura des problèmes.

Je fais quelque chose comme ça pour faire ce que tu veux.

   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>>();
        }
    }
4
Filip Cordas

Il existe une bibliothèque appelée Communist.StartupTasks qui gère ce scénario exact. Il est disponible sur Nuget. 

Il est spécialement conçu pour exécuter des tâches lors du lancement d'une application dans une application .NET Core. Il supporte pleinement l'injection de dépendance.

Veuillez noter qu'il exécute les tâches de manière séquentielle et qu'il se bloque jusqu'à ce que toutes les tâches soient terminées (c'est-à-dire que votre application n'acceptera pas les demandes jusqu'à la fin des tâches de démarrage).

1
Brad