如何在MVC Core应用程序中启动HostedService而无需HTTP请求

18
在我的MVC.NET Core 2.2应用程序中,有一个执行后台工作的HostedService。它在Startap类的ConfigureServices方法中注册。
services.AddHostedService<Engines.KontolerTimer>();

由于这是独立于用户请求的后台服务,我希望在应用程序启动时立即启动后台服务。现在我的HostedService要等待第一个用户请求才会启动。

如何正确地在MVC Core应用程序启动时启动HostedService?

我的服务看起来像这个https://learn.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();
    }
}
看起来我完全无法启动该应用程序。
我的程序代码看起来像这样:
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>();
    }

而且在第一个用户请求之前,我没有碰到任何断点。这是默认的.Net Core应用程序,由VS2017创建。

这里是我的 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>();}

你是如何进行主机托管的?我在将HostedService部署到IIS时遇到了麻烦。 - Adrita Sharma
1
@AdritaSharma,要让应用程序在IIS中自动启动,您需要使用应用程序初始化。https://dev59.com/aaXja4cB1Zd3GeqPUquJ - Eric
2
在IIS v10和ASP.NET Core 5上花费了一整天的时间解决同样的问题后,我终于找到了解决方案。除了应用程序初始化之外,您还需要将应用程序池设置为“Startmode=AlwaysRunning”,并将站点设置为“Preload Enabled=true”(两者都可以在高级设置中找到)。另请参见https://dev59.com/aaXja4cB1Zd3GeqPUquJ#46573873。 - Soko
5个回答

16

当你在使用Visual Studio运行的时候,你可能在使用IIS Express,这不会运行你的ASP.NET Core项目直到第一个请求被发起(这实际上是IIS默认工作方式)。对于使用InProcess hosting-model的ASP.NET Core2.2新功能来说,这同样适用于你遇到的这个问题。请参阅此GitHub问题了解更多信息。

你可以通过从.csproj文件中删除AspNetCoreHostingModel XML元素来验证这一理论,该文件用于托管ASP.NET Core应用程序(这将将其切换回OutOfProcess模式)。如果你不想直接编辑.csproj文件,那么在VS2017项目属性对话框中的“调试”下有一个“托管模型”选项,可以将其更改为“Out Of Process”。

如果你想让托管模型仅适用于生产站点,则可以使用Web.config转换。如果你想在开发和生产过程中都使用OutOfProcess模式,只需更改我上面提到的属性即可,因为它会自动转换为Web.config属性。如果你更喜欢使用in-process模型,启用IIS应用程序中的预加载是一个不错的选择(在此处有描述)。


1
使用 Out of Process 在 Visual Studio 中运行时可以正常工作,但在 IIS 中托管时却没有任何效果。对于 IIS,您必须使用 Application Initialization。https://dev59.com/aaXja4cB1Zd3GeqPUquJ - Eric

2

背景服务在您的应用程序启动时启动,然后由您负责与其同步。

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

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

Original Answer翻译成“最初的回答”

public interface IMyService : IHostedService
{
}

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

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中注册它:

Original Answer翻译成“最初的回答”

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

这已足以启动服务。请注意,如果在IIS中托管应用程序,则实际上可能会在稍后的时间启动您的应用程序:每当您的程序集被回收时,您的应用程序就会被(重新)启动。相反,使用Kestrel提供一个单一实例应用程序,它将不会被回收。
对于那些使用 .Net Core 2.1 或更低版本的人来说,Background类不可用,但是您可以从github获取定义(我发布了过去使用的内容,因为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();
    }
}

你是如何进行主机托管的?我在将HostedService部署到IIS时遇到了麻烦。 - Adrita Sharma
1
我在Linux上使用了Kestrel和Nginx,但我还没有尝试过使用IIS。在我看来,需要注意的是,当服务被回收时(例如在IIS中),这些任务最好不要执行:因为应用程序域的回收会导致触发这些事件的频率超过正常情况,从而可能产生意想不到的后果。 - Yennefer

0

0

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

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

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


-1

托管服务在主机启动时会启动。使用 WebHost,托管服务将在应用程序启动后立即启动。这意味着,如果实现正确,您的托管服务将无需等待请求即可运行。

当我在全新的 ASP.NET Core 应用程序上尝试您的示例托管服务时,它可以正常工作,因此如果对您而言无法正常工作,则显然您的实际实现KontolerTimer不正确。


1
奇怪的是,在 ConfigureServices 的第一行中我没有触发断点,直到我进行了第一个请求。我在 Visual Studio 中关闭了属性中的启动网页,但当我在 IIS 中部署我的应用程序时,同样的情况也发生了。在第一个 HTTP 请求之前,没有调用 StarUp 方法。 - adopilot
是的,当使用iis express时它们肯定不会出现。在直接使用iis或kestrel时可能会有不同的行为。 - Sam
你是如何托管它的?我在将 HostedService 部署到 IIS 中遇到了麻烦。 - Adrita Sharma

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接