如何在没有 http 请求的情况下在 MVC 核心应用程序中启动 HostedService

How to start HostedService in MVC Core app without http request

在我的 MVC .NET core 2.2 应用程序中,有一个 HostedService 正在执行后台工作。

在Startap的ConfigureServices方法中注册class

services.AddHostedService<Engines.KontolerTimer>();

因为这是独立于用户请求的后台服务,所以我想在应用程序启动时立即启动我的后台服务。 现在是我的 HostedService 在第一次用户请求后启动的情况。

当 MVC 核心应用程序启动时启动 HostedService 的正确方法是什么

我的服务看起来像这个 https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-2.2

internal class TimedHostedService : IHostedService, IDisposable
{
    private readonly ILogger _logger;
    private Timer _timer;

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

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Timed Background Service is starting.");

        _timer = new Timer(DoWork, null, TimeSpan.Zero, 
            TimeSpan.FromSeconds(5));

        return Task.CompletedTask;
    }

    private void DoWork(object state)
    {
        _logger.LogInformation("Timed Background Service is working.");
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Timed Background Service is stopping.");

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

        return Task.CompletedTask;
    }

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

看来我完全无法凝视应用程序。

我的程序 cs 看起来像

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


        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
            .UseSerilog((ctx, config) => { config.ReadFrom.Configuration(ctx.Configuration); })
            .UseStartup<Startup>();
    }

并且在第一个用户请求之前我没有遇到任何断点。 我错过了什么吗,这是由 VS2017

创建的默认 .Net Core 应用程序

这是我的 starup.cs

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

        public IConfiguration Configuration { get; }
        private Models.Configuration.SerialPortConfiguration serialPortConfiguration;

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<CookiePolicyOptions>(options =>
            {
                // This lambda determines whether user consent for non-essential cookies is needed for a given request.
                options.CheckConsentNeeded = context => true;
                options.MinimumSameSitePolicy = SameSiteMode.None;
            });

            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(
                    Configuration.GetConnectionString("DefaultConnection")));

            services.AddIdentity<ApplicationUser, ApplicationRole>(options => options.Stores.MaxLengthForKeys = 128)
                .AddDefaultUI(UIFramework.Bootstrap4)
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders();

            services.AddDbContext<Data.Parking.parkingContext>(options =>
                options.UseSqlServer(
                    Configuration.GetConnectionString("DefaultConnection")));


         services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
            services.AddHostedService<Engines.KontolerTimer>();}

托管服务do 在主机启动时启动。使用 WebHost,托管服务 will be started right after the application has started。这意味着如果实施正确,您的托管服务将 运行 无需请求进入。

当我在一个新的 ASP.NET 核心应用程序上尝试您的示例托管服务时,它工作得很好,所以如果它不适合您,那么显然您的 actual 实现 KontolerTimer 不正确。

如果您想让服务执行后台任务(类似于旧的 Windows 服务),我建议您使用:https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host?view=aspnetcore-2.2 而不是 WebHost。

WebHost 添加了很多您可能不需要的东西,因为这似乎是一项简单的后台工作(假设阅读您的代码)。

后台服务在您的应用程序启动时启动,然后由您与它同步。

您可以使用命名空间 Microsoft.Extensions.HostingMicrosoft.Extensions.Hosting.Abstractions 程序集)中的 BackgroundService class 实现后台服务:

首先声明您的服务接口(在本例中它是空的,不是很好,但是很干净):

public interface IMyService : IHostedService
{
}

然后,声明您的服务。以下代码段声明了一个服务,在启动时等待 5 秒,然后每 2 分半钟执行一个任务:

internal sealed class MyService : BackgroundService, IMyService
{
    private const int InitialDelay = 5 * 1000;  //5 seconds;
    private const int Delay = (5 * 60 * 1000) / 2; // 2.5 minutes

    private readonly ILogger<MyService> m_Logger;

    public MyService(ILogger<MyService> logger, IServiceProvider serviceProvider)
    {
        if (logger == null)
            throw new ArgumentNullException(nameof(logger));
        if (serviceProvider == null)
            throw new ArgumentNullException(nameof(serviceProvider));

        this.m_Logger = logger;
        this.m_ServiceProvider = serviceProvider;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        try
        {
            m_Logger.LogDebug($"MyService is starting.");

            stoppingToken.Register(() => m_Logger.LogDebug($"MyService background task is stopping because cancelled."));

            if (!stoppingToken.IsCancellationRequested)
            {
                m_Logger.LogDebug($"MyService is waiting to be scheduled.");
                await Task.Delay(InitialDelay, stoppingToken);
            }

            m_Logger.LogDebug($"MyService is working.");

            while (!stoppingToken.IsCancellationRequested)
            {
                await DoSomethingAsync();

                await Task.Delay(Delay);
            }

            m_Logger.LogDebug($"MyService background task is stopping.");
        }
        catch (Exception ex)
        {
            m_Logger.LogDebug("MyService encountered a fatal error while w task is stopping: {Exception}.", ex.ToString());
        }
    }

    private async Task DoSomethingAsync()
    {
         // do something here
         await Task.Delay(1000);
    }
}

如您所见,让后台服务保持“活跃”取决于您。最后,您必须在 ConfigureServices 方法的末尾在 Startup.cs 中注册它:

services.AddSingleton<Microsoft.Extensions.Hosting.IHostedService, MyService>();

这足以启动服务。请记住,如果托管在 IIS 中,您的应用程序可能会在稍后实际启动:每次回收程序集时,您的应用程序都会(重新)启动。相反,使用 Kestrel 提供了一个不会被回收的单一实例应用程序。

对于那些使用 .Net Core 2.1 或更低版本的用户,背景 class 不可用,但您可以从 github 中获得定义(我 post 我在过去,因为 github 存储库可以移动):

//borrowed from .NET Core 2.1 (we are currently targeting 2.0.3)
// Copyright (c) .NET Foundation. Licensed under the Apache License, Version 2.0.
/// <summary>
/// Base class for implementing a long running <see cref="IHostedService"/>.
/// </summary>
public abstract class BackgroundService : IHostedService, IDisposable
{
    private Task _executingTask;

    private readonly CancellationTokenSource _stoppingCts =
                                                   new CancellationTokenSource();

    protected abstract Task ExecuteAsync(CancellationToken cancellationToken);

    public virtual Task StartAsync(CancellationToken cancellationToken)
    {
        // Store the task we're executing
        _executingTask = ExecuteAsync(_stoppingCts.Token);

        // If the task is completed then return it,
        // this will bubble cancellation and failure to the caller
        if (_executingTask.IsCompleted)
        {
            return _executingTask;
        }

        // Otherwise it's running
        return Task.CompletedTask;
    }

    public virtual async Task StopAsync(CancellationToken cancellationToken)
    {
        // Stop called without start
        if (_executingTask == null)
        {
            return;
        }

        try
        {
            // Signal cancellation to the executing method
            _stoppingCts.Cancel();
        }
        finally
        {
            // Wait until the task completes or the stop token triggers
            await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite,
                                                          cancellationToken));
        }
    }

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

当您 运行 使用 Visual Studio 时,您可能正在使用 IIS Express,它不会 运行 您的 ASP.NET 核心项目,直到第一个请求已完成(这实际上就是 IIS 默认情况下的工作方式)。这适用于使用 ASP.NET Core 2.2 新增的 InProcess 托管模型,我希望您必须使用它才能看到此问题。有关更多信息,请参阅此 GitHub issue

您可以通过从用于托管 ASP.NET 核心应用程序的 .csproj 文件中删除 AspNetCoreHostingModel XML 元素来证明这一理论(这会将其切换回 OutOfProcess 模式). VS2017的项目属性对话框中的"Debug"下好像有一个"Hosting Model"选项,如果不想直接编辑.csproj可以改成"Out Of Process"[=12] =]

如果您希望托管模型仅针对生产站点是进程外的,例如,您可以使用 Web.config 转换。如果您希望它在开发和生产过程中都处于进程外,只需更改我上面提到的 属性 就足够了,因为它会自动转换为 Web.config 属性 .如果您更愿意使用进程内模型,则在 IIS 应用程序中启用预加载是一个不错的选择(描述 here)。

对我来说...后台任务直到第一个页面请求才开始。

但后来我注意到在我的发布/编辑中,我没有设置目标 Url。 (而且我也没有主页索引页)...

一旦我添加了一个有效的目标 Url...该页面将在发布后弹出并成为我的“第一个”页面请求,后台任务将启动。