ASP.NET Core 用 Hangfire 实现定时任务周期任务

发布时间 2023-05-22 09:22:09作者: AI大胜

前言

系统经常会有一些逻辑或任务,需要定时执行或周期性执行,为了实现这个需求,一般有以下几种方式选择:

  • 后台任务,即 IHostedService
  • 开源库:Hangfire
  • 开源库:Quartz.net

这里,说下Hangfire的使用。

官网

https://www.hangfire.io/

Hangfire – Background jobs and workers for .NET and .NET Core 。

An easy way to perform background processing in .NET and .NET Core applications. No Windows Service or separate process required.

Backed by persistent storage. Open and free for commercial use.

是什么、为什么

它就是一个开源库,用于在程序中,定时做些什么,或周期性的做些什么。

使用它,让在.net应用中,编写任务调度类似的功能,更方便快速。比如在ASP.NET Core中,不再需要自己基于 IHostedService和计时器等,完整的写完一套定时或周期性任务。

以下是它可以做的事:

image-20230522083620030

其它

首先,它适用于任何.NET平台(.NET Framework 4.5 or later, .NET Core 1.0 or later, or any platform compatible with .NET Standard 1.3),与特定的.NET应用程序类型无关。

Hangfire通常在IIS进程中运行(尽管它也可以在控制台应用程序中运行)。 Hangfire的主要优点是开发人员无需登录IIS服务器即可创建计划任务。

一些特征和优点:

i0

重要概念:

Storage:一个后台任务相关信息的存储工具,一个任务的类型,方法,参数等都经序列化后放在Storage里。这个Storage可以是关系型数据库或非关系型数据库。

在ASP.NET Core中的使用(用SQL Server作为Storage)

安装NuGet包:

dotnet add package Hangfire.Core
dotnet add package Hangfire.SqlServer
dotnet add package Hangfire.AspNetCore

创建一个新的数据库当做Storage,或使用一个原有的数据库,

这里的后端指的是任务队列的存储后端,也就是 Hangfire 文档中写的 Storage。看了一下官网,大部分关系型非关系型数据库都是可以的。

这里先使用SQL Server。

Corn 表达式

在使用HangFire前,得先了解Corn表达式的使用:

如何在ASP.NET Core MVC 中使用

安装Nuget

dotnet add package Hangfire.Core
dotnet add package Hangfire.SqlServer
dotnet add package Hangfire.AspNetCore

这里先使用SQL Server,作为相关任务信息的数据存储。

先定义两个扩展方法,用于相关服务注册、和作为中间件使用

这样可以让程序的 startup.cs 或 program.cs 更加简洁清晰,提高可读性和减少更改。

比如在 HangfireExtension 类中定义:

    public static class HangfireExtension
    {
        public static void AddMyHangfire(this IServiceCollection services, IConfiguration configuration)
        {
            services.AddHangfire(configuration => configuration
                    .SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
                    .UseSimpleAssemblyNameTypeSerializer()
                    .UseRecommendedSerializerSettings()
                    .UseSqlServerStorage("Hangfire的数据库链接字符串").value, new SqlServerStorageOptions
                    {
                        CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
                        SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
                        QueuePollInterval = TimeSpan.Zero,
                        UseRecommendedIsolationLevel = true,
                        DisableGlobalLocks = true
                    }));

            // Add the processing server as IHostedService
            services.AddHangfireServer();
            //Hangfire的数据库可以跟应用程序共用一个库,也可以单独用一个数据库,里面大概不到10个表。
        }

        public static void UseMyHangFire(this IApplicationBuilder app, bool isUseDashboard = true)
        {
            if (isUseDashboard)
            {//是否使用web页面的后台管理
                //配置控制台页面路径,以及生产环境配置账号密码登录验证。
                app.UseHangfireDashboard("/hangfire", GlobalContext.HostingEnvironment.IsDevelopment() ? null : new DashboardOptions
                {
                    Authorization = new[] { new HangfireDashboardLoginFilter() }
                });
            }

            //定时数据库备份。
            RecurringJob.AddOrUpdate("Db_Backup",() => DbBackupTask.Db_Backup(), "0 0 6,12,18,23 * * ?");

            #region 测试几个
            if (false)
            {
                RecurringJob.AddOrUpdate("test_log", () => LogHelper.Info("测试:周期性打印日志", null), "0/5 * * * * *");//实际大概15秒执行一次日志打印。
                var jobId1 = BackgroundJob.Enqueue(() => Console.WriteLine("立马执行一次!" + DateTime.Now.GetDateTimeDesc()));
                var jobId2 = BackgroundJob.Schedule(() => Console.WriteLine("Delayed!5秒后执行一次。" + DateTime.Now.GetDateTimeDesc()), TimeSpan.FromSeconds(5));

                RecurringJob.AddOrUpdate("myrecurringjob", () => Console.WriteLine("反复执行,每5分钟!" + DateTime.Now.GetDateTimeDesc()), "*/5 * * * *");

                RecurringJob.RemoveIfExists("some-id");// It does not throw an exception when there is no such recurring job.
                                                       //RecurringJob.Trigger("some-id");//如果你想随时执行一个需重复间隔一定时间才执行的方法,那就用这个,不影响他原有的定期执行计划。
            }
            #endregion
        }

    }

定义个用于控制台需登录授权访问的类

一个控制台相关的类,用于设置进入页面的账号密码和身份验证,是个安全措施。

    public class HangfireDashboardLoginFilter : IDashboardAuthorizationFilter
    {
        public bool Authorize(DashboardContext context)
        {
            var httpContext = context.GetHttpContext();
            string header = httpContext.Request.Headers["Authorization"];//获取授权
            if (header == null)
                return AuthenicateLogin();
            //解析授权
            var authHeader = AuthenticationHeaderValue.Parse(header);
            var credentialBytes = Convert.FromBase64String(authHeader.Parameter);
            var credentials = Encoding.UTF8.GetString(credentialBytes).Split(new[] { ':' }, 2);
            var username = credentials[0];
            var password = credentials[1];
            //验证登录
            if (username == "账号" && password == "密码")
                return true;
            else
                return AuthenicateLogin();
            //跳转简单登录界面
            bool AuthenicateLogin()
            {
                httpContext.Response.StatusCode = 401;
                httpContext.Response.Headers.Append("WWW-Authenticate", "Basic realm=\"Hangfire Dashboard\"");
                context.Response.WriteAsync("Authenticatoin is required.");
                return false;
            }

        }
    }

添加服务注册和加入到中间件配置

在startup.cs 或 program.cs 中

//服务注册
//Add Hangfire services.
services.AddMyHangfire(Configuration);

//向请求处理管道中添加中间件
//使用Hangfire
app.UseMyHangFire();

app.UseRouting();

补充上面提到的定时备份数据库的任务类

    public class DbBackupTask
    {
        public static void Db_Backup()
        {
            var dal = new SystemDAL();
            var saveFolderPath = @"D:\DbBackup";
            using (var conn=dal.GetDbConnection())
            {
                saveFolderPath += $@"\{conn.Database}";
            }
            
            if (!Directory.Exists(saveFolderPath))
            {
                Directory.CreateDirectory(saveFolderPath);
            }

            var box = new DirectoryInfo(saveFolderPath);
            var files = box.GetFiles();
            if (files.Length > 20)
            {
                var delItems = files.Where(x => x.CreationTime < DateTime.Now.AddDays(-7));
                foreach (var item in delItems)
                {
                    item.Delete();
                }
            }
            dal.BackupDB(saveFolderPath);
        }
    }

使用时的一个注意点:

如果程序部署在iis,请务必查看下面这篇文章,以及HangFire官网相关的在IIS上部署相关的注意事项:

IIS 禁止自动回收 - PrintY - 博客园 (cnblogs.com)


更新于:2023.5.22