ASP.NET Core MVC 定时任务

18

我需要在 asp.net mvc core 应用程序中每天运行一次定时任务。 我该如何实现呢?

谢谢


1
https://blog.maartenballiauw.be/post/2017/08/01/building-a-scheduled-cache-updater-in-aspnet-core-2.html - ravish.hacker
尝试这个答案 https://dev59.com/vVIH5IYBdhLWcg3wFInR - Muneeb Mirza
8个回答

14

2022年11月更新答案

DotNet Core 2推出了一种新的接口IHostedService,可用于处理后台任务。在这个微软文档中,您可以找到有关在ASP.NET Core中使用托管服务实现后台任务的详细信息:Background tasks with hosted services in ASP.NET Core

一个简单的HostedService实现:


    public class SimpleHostedService : IHostedService
    {
        private readonly ILogger<SimpleHostedService> _logger;

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

        public async Task StartAsync(CancellationToken stoppingToken)
        {
            _logger.LogInformation("Timed Hosted Service Started.");

            while (!stoppingToken.IsCancellationRequested)
            {
                DoWork();

                // Wait one second
                await Task.Delay(1000);
            }

        }

        private void DoWork()
        {
            // Do something
        }

        public Task StopAsync(CancellationToken stoppingToken)
        {
            _logger.LogInformation("Hosted Service is stopping.");

            return Task.CompletedTask;
        }
    }

关于计划任务,我将展示两个选项:

  1. 使用开箱即用的System.Threading.Timer来触发任务的DoWork方法。请查看定时后台任务
public class TimedHostedService : IHostedService, IDisposable
{
    private int executionCount = 0;
    private readonly ILogger<TimedHostedService> _logger;
    private Timer? _timer = null;

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

    public Task StartAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Timed Hosted Service running.");

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

        return Task.CompletedTask;
    }

    private void DoWork(object? state)
    {
        var count = Interlocked.Increment(ref executionCount);

        _logger.LogInformation(
            "Timed Hosted Service is working. Count: {Count}", count);
    }

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

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

        return Task.CompletedTask;
    }

    public void Dispose()
    {
        _timer?.Dispose();
    }
}
  1. 可以使用类似于 Cron 项目 ServiceWorkerCronJob ,实现更完整和灵活的特定计划配置。 这篇文章 展示了该组件的强大功能以及如何实现。

我的建议和鼓励

将所有的 BackgroudServices 放在一个专门的 WorkerService 应用程序中,可以查看一下 .NET 的 Worker Services

在同一个项目中同时实现 BackgroundServicesAspNet Web/Api (我指的是)会带来很多复杂性,当您需要进行 Scale Out 时更是如此。因此,请记住:

  1. 处理 CPU 密集型或内存密集型可能会影响 AspNet 响应时间的性能。
  2. 扩展到多个实例将在每个实例上复制 BackgroundService 执行,并导致不必要的错误或浪费资源。
  3. 随着时间的推移,应用程序的业务规则往往会增长,在未来您可能需要处理上述问题。

8

简短回答

你不能。

详细回答

你需要使用第三方库,例如 Quarz、Hangfire 或 Azure WebJobs,它们可以从 ASP.NET Core 应用程序内部或外部触发。

但请注意,如果使用 Quarz 或 Hangfire 在 ASP.NET Core 应用程序内部运行它,它可能会受到进程生命周期的影响,即如果在 IIS 或 Azure 应用服务上运行它,则你无法控制 IIS 何时停止应用程序(由于不活动或其他原因)或是否在没有外部请求的情况下启动它(IIS 可以配置为立即重新启动应用程序,缺省设置是在下一次请求时)。

也就是说,当 IIS 或其他进程关闭应用程序时,最好跳过触发器。因此,最好让计划程序在 ASP.NET Core 进程之外运行(例如,在控制台应用程序中、在 Azure 上的后台工作程序中或使用 Azure Web Jobs)。


这是真的吗?我刚想在 host.Run() 之前将一个使用 C# 的 System.TaskScheduler 放入 Program.cs 中。 - Ole K
2
@OleK:在ASP.NET中启动线程是一个相当糟糕的想法,首先它可能会减少可用于请求的线程数(它们与应用程序共享),从而限制性能/降低它可以处理的请求数。其次,ASP.NET应用程序可以随时被回收,特别是在Azure App Service或IIS上运行时。如果您使用依赖注入并且某些服务是作用域的,则需要自己管理这些作用域(通常ASP.NET为每个请求创建1个作用域,当您在请求之外运行时,您必须自己管理)。 - Tseng
为了避免在IIS上的回收,可能有一种选项可以停止类似于我在这里指出的回收:https://dev59.com/KJzha4cB1Zd3GeqPCknM#41263683 - Ole K
@OleK:这是没有保证的。你只能通过这种方式改变空闲时间,但如果你的IIS资源不足,无论设置了什么选项,它都会重新启动。当发生这种情况时,你无法控制它何时再次启动。而且,当你编辑web.config时,IIS将强制重新启动/回收你的应用程序,无论如何。在ASP.NET应用程序内部做这种事情并不是一个好的实践。有其他选项(consoll应用程序、Windows服务等)比这更适合和稳健。 - Tseng
为了解决这个问题,只需在appsettings.json中记录任务是否已完成,例如与当前时间进行比较,它将在下一次启动时立即完成。它无法解决关键计时器,如工作闹钟,但对于大多数应用程序中的状态而言,它已经足够可用,在执行着陆页面之前请检查它。 - Peter

3

这是一个老问题,但我看到了最新框架版本的很多改进,却没有太多关于如何以清晰的方式使其正常工作的信息。

AspNet Core 3.x以比以往更抽象的方式实现了Web主机生成器,允许完美运行不同类型的服务。Web容器的初始化与运行SignalR服务器、Web Api、MVC或工作线程的方式相同,因此我们可以将它们混合使用而没有问题。

例如,以下方式初始化API/MVC应用:

public static IHostBuilder CreateHostBuilder(string[] args)
{
    return Host.CreateDefaultBuilder(args)
               .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
}

我们可以使用模板在通常的启动代码中声明任何类型的服务。按照您需要的方式配置服务,它们将为任何托管的应用程序准备就绪。现在,让我们添加一个定时工作程序,我们需要创建一个简单的类,遵循Microsoft的文档:

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

来源:Microsoft

在 Program.cs 文件中将我们的 worker 添加到构建器中:

public static IHostBuilder CreateHostBuilder(string[] args)
{
    return Host.CreateDefaultBuilder(args)
               .ConfigureServices((hostContext, services) =>
               {
                   services.AddHostedService<TimedHostedService>();
               })
               .ConfigureWebHostDefaults(webBuilder =>
               {
                   webBuilder.UseStartup<Startup>();
               });
}

您的网络应用程序将启动,运行正常服务以接受请求,同时您还将获得定时主机服务。优点是您还可以获得依赖注入,只需向TimedHostService构造函数添加任何接口,该接口将被框架透明地注入。

3

作为一个使用 System.Threading.Timer 的 Startup.cs 的想法。

// add the below into your Startup contructor
public Startup(IHostingEnvironment env)
{
    // [...]
    var t = new System.Threading.Timer(doSomething);
    // do something every 15 seconds
    t.Change(0, 15000);
}

// define a DateTime property to hold the last executed date
public DateTime LastExecuted { get; set; }

// define the doSomething callback in your Startup class
private void doSomething(object state)
{
    System.Diagnostics.Debug.WriteLine("The timer callback executes.");

    // this callback is called every 15 seconds
    // but the condition below makes sure only it
    // only takes place at 17:00 
    // every day, when LastExecuted is not today
    if(DateTime.UtcNow.Hour == 17 && DateTime.UtcNow.Minute == 0 && DateTime.UtcNow.Date != LastExecuted.Date)
    {
        LastExecuted = DateTime.UtcNow;
        System.Diagnostics.Debug.WriteLine("#### Do the job");
    }
}

使用这种方法注入服务时,使用DI/IoC会遇到麻烦,因为在启动时容器尚未设置,并且像DbContext这样的作用域服务也是如此。如果在其中启动线程,可能会完全破坏ASP.NET管理线程池的方式。 - Tseng
@Tseng 我完全同意,但问题实际上并不关注使用DBContext的任何服务。或者我们甚至可以将这些行放入单例服务中... - Ole K

2
我建议运行一个 PowerShell 脚本,向应用程序中的任何 API/端点发出 GET 请求。这样,您将可以在 .NET Core 应用程序中使用基于 Windows 的任务计划程序。
1. 创建一个 PowerShell 脚本并将其保存在服务器上,命名为 myjob.ps1。
$url="http://www.myapp.com/api/runjob"
$content=(New-Object System.Net.WebClient).DownloadString("$url");

$Logfile = "D:\Temp\job.log"
Add-content $Logfile -value $content

2. 将powershell执行策略设置为无限制

打开Powershell并运行以下命令:

Set-Executionpolicy -Scope CurrentUser -ExecutionPolicy UnRestricted
Set-Executionpolicy -Scope Process -ExecutionPolicy UnRestricted

请点击以下链接了解如何设置定时任务:https://www.metalogix.com/help/Content%20Matrix%20Console/SharePoint%20Edition/002_HowTo/004_SharePointActions/012_SchedulingPowerShell.htm 3. 创建一个定时任务并执行你的脚本。
在任务计划程序中创建新任务。在创建操作时,将其设置为“启动程序”。
Program: Powershell
Argument: -noprofile -executionpolicy bypass -file "C:\Jobs\myjob.ps1"

1
如果您将代码托管在Azure上,类似这样的操作是可能的。使用Azure WebJobs,您可以像cronjob一样请求特定的URL,但如果需要,也可以运行特定的代码段。如果需要,这可以是一个asp.net core应用程序。但是,您需要更改VS为您创建的启动项目中的一些内容。
例如,监听队列并在排队时发送电子邮件。或从大图像创建缩略图。
更多信息可以在此处找到:https://azure.microsoft.com/en-us/documentation/articles/websites-dotnet-webjobs-sdk-get-started/

0

-1

通常,如果您不想使用Azure或任何其他服务,可以创建一个新项目。一种规则引擎。计划任务将存储在数据库中(例如SQL Server),然后应用程序将读取此任务并运行逻辑。因此,通常最好有一个不同的应用程序来处理定期任务(如清理文件、电子邮件等)。这种类型的解决方案使您能够轻松地将逻辑扩展到多种类型的任务并应用规则。例如,根据您决定的各种规则发送电子邮件。


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