在上1章中定义Jquery DataTables插件的后台实现,但是nopCommerce自定义Jquery DataTables插件的完整实现,还缺乏相应的以后台代码定义(C#代码)的前台渲染界面(UI),nopCommerce中Jquery DataTables插件前台渲染界面(UI)定义(C#代码)包含有:
- Jquery DataTables插件插件的后台定义(C#代码)。
- JavaScript 的后台定义(C#代码)。
- CSS的后台定义(C#代码)。
- HTML标签控件台定义(C#代码)。
从上面的要求来看nopCommerce自定义Jquery DataTables插件的前台渲染界面(UI)需要大量的后台定义来实现,其实现方法十分复杂,与nopCommerce耦合比较紧密,不利于其它程序的复用、迁移和移植,如果要使用通用型的Jquery DataTables插件,本人认为最好自己自定义1个。由于时间问题本人就不再讲解nopCommerce自定义Jquery DataTables插件的完整实现了,但是如果有足够的时间本人还是会补上的,毕竟这个是nopCommerce比较常用的功能。
1 计划任务(ScheduleTasks)
1 突兀的计划任务(ScheduleTasks)
在nopCommerce程序中计划任务(ScheduleTasks)的定义实现,与其它服务定义实现,在初次接触时相比显得十分的突然、突兀和不知所云。
由于SQL Server数据库中也存在“Schedule”关键字同时也可以定义存储过程来定时自动触发相应的数据库操作,例如数据库的定时备份操作等,nopCommerce程序中计划任务(ScheduleTasks)在数据库中也存在相应的表,所以当我在对计划任务深入研究之前,我一直认为计划任务(ScheduleTasks)的功能是对数据库进行管理;但是在我对计划任务深入研究之后,二者风马牛不相及,者唯一的相同点就是都可以自动触发相应操作。
2 计划任务(ScheduleTasks)的功能
nopCommerce程序通过计划任务(ScheduleTasks)来实现,程序中需要在定期时间内所需要完成的固定性的功能或工作。
3 计划任务(ScheduleTasks)的原理
nopCommerce程序中计划任务(ScheduleTasks) 原理是:被.Net(Core)内置管道中间件所调用,以保证在程序开始启动执行时,实例化当前程序中的所有计划任务实例,并为这些任务实例构建相应的线程实例;并保证在程序其后的执行过程中,定时器(Timer)实例。会在指定的时间间隔之间,自动出触发执行“ScheduleTaskController.RunTask”方法来实现计划任务实例的自动执行。
nopCommerce程序中触发指定计划任务服务的方式是:把当前程序定义的发指定计划任务服务,定义为第3方服务(由HttpClient实例所调用),再有当前程序通过定时器(Timer)方法成员自动触发并执行后,通过移步任务(Task)状态结果来确认(获取)指定计划任务的执行状态的结果。
注意:
第3方服务(由HttpClient实例所调用)常用的场景是:当前程序调用其它的程序、网站、APP等。例如nopCommerce程序中调用Google机器人验证和Google和Facebook的授权登录等,但是计划任务服务却是在当前程序中进行定义,并作为第3方服务由当前程序所调用。
下面本人将以持久连接 (KeepAliveTask) 计划任务的实现为示例,来讲解nopCommerce程序中计划任务(ScheduleTasks)是怎样具体实现的。
2 计划任务(ScheduleTasks)实体与EFCore上下文
1 计划任务(ScheduleTasks)实体的定义实现
/// <summary>
/// 【计划任务--类】
/// <remarks>
/// 摘要:
/// 通过该实体类及其属性成员,实现当前程序实现当前程序【Json】.【领域】.【计划任务集】.【计划任务】实体与“[JsonTable].[ScheduleTask]”表之间的CURD的交互操作。
/// </remarks>
/// </summary>
public partial class ScheduleTask
{
/// <summary>
/// 【编号】
/// <remarks>
/// 摘要:
/// 获取/设置计划任务实体1个指定实例的长整型编号值。
/// </remarks>
/// </summary>
public long Id { get; set; }
/// <summary>
/// 【名称】
/// <remarks>
/// 摘要:
/// 获取/设置1个指定计划任务实体实例的名称。
/// </remarks>
/// </summary>
public string Name { get; set; }
/// <summary>
/// 【间隔时间(秒)】
/// <remarks>
/// 摘要:
/// 获取/设置当前程序通过定时器方法自动触发执行同1个指定计划任务实例的时间间隔整型值(单位:秒)。
/// </remarks>
/// </summary>
public int Seconds { get; set; }
/// <summary>
/// 【类型】
/// <remarks>
/// 摘要:
/// 获取/设置1个指定计划任务实例的类型实例信息,该计划任务类必须是继承于“IScheduleTask”接口的,类型实例信息例如:“JsonTable.Services.Common.KeepAliveTask(指定计划任务类全名), JsonTable(库名或启动项名)”。
/// </remarks>
/// </summary>
public string Type { get; set; }
/// <summary>
/// 【启用?】
/// <remarks>
/// 摘要:
/// 获取/设置1个值false(禁用)/true(启用),该值指示1个指定计划任务实例是否处于启用状态,即当前程序是否能够通过定时器方法自动触发执行该计划任务。
/// </remarks>
/// </summary>
public bool Enabled { get; set; }
/// <summary>
/// 【发生错误时停止?】
/// <remarks>
/// 摘要:
/// 获取/设置1个值false(运行)/true(停止),该值指示1个指定计划任务实例处于执行失败状态时,当前程序是否能够自动停止该计划任务。
/// </remarks>
/// </summary>
public bool StopOnError { get; set; }
/// <summary>
/// 【最后启用日期时间】
/// <remarks>
/// 摘要:
/// 获取/设置1个指定计划任务实例,最后1次被启用的时间。
/// </remarks>
/// </summary>
public DateTime? LastEnabled { get; set; }
/// <summary>
/// 【最后开始日期时间】
/// <remarks>
/// 摘要:
/// 获取/设置1个指定计划任务实例,被当前程序通过定时器方法最后1次自动触发的开始时间。
/// </remarks>
/// </summary>
public DateTime? LastStart { get; set; }
/// <summary>
/// 【最后结束日期时间】
/// <remarks>
/// 摘要:
/// 获取/设置1个指定计划任务实例,被当前程序通过定时器方法最后1次自动触发的结束时间(不管指定计划任务实例执行的状态是成功或失败,该属性成员实例都要重新存储该时间值,并持久化到数据库指定表的字段中)。
/// </remarks>
/// </summary>
public DateTime? LastEnd { get; set; }
/// <summary>
/// 【最后成功日期时间】
/// <remarks>
/// 摘要:
/// 获取/设置1个指定计划任务实例,被当前程序通过定时器方法最后1次自动触发的结束时间(只有指定计划任务实例执行的是成功状态,该属性成员实例才重新存储该时间值,并持久化到数据库指定表的字段中;即如果是失败状态该属性成员实例只存储最后1次执行成功时的时间值)。
/// </remarks>
/// </summary>
public DateTime? LastSuccess { get; set; }
}
2 重构FCore上下文类
/// <summary>
/// 【EFCore上下文--类】
/// <remarks>
/// 摘要:
/// 该类通过“EntityFrameworkCore”中间件,实现当前程序与指定数据库软件(这里特指:SQL Server数据库软件)数据库中所有表的CURD操作。
/// </remarks>
/// </summary>
public class EFCoreContext : DbContext
{
#region 拷贝构造方法
///<param name="options">数据库依赖注入中间件自定义上下文实例,通过该实例中的数据库连接字符串,与指定数据库软件中的相应数据库进行连接;或在指定数据库软件中的自动生成相应的数据库。</param>
/// <summary>
/// 【拷贝构造方法】
/// <remarks>
/// 摘要:
/// 基类构造通过该构造方法中的参数实例,连接到指定数据库(SQL Server)数据库软件中数据库。
/// </remarks>
/// </summary>
public EFCoreContext(DbContextOptions<EFCoreContext> options) : base(options)
{
//如果(SQL Server)数据库软件中没有指定的数据库, 当通过Code First模式时,在第1次生成数据库时,则通过下1行语句结合数据库连接字符串,在(SQL Server)数据库软件中生成指定的数据库数据库、表、字段和约束规则。
Database.EnsureCreated();
/*
如果(SQL Server)数据库软件中没有指定的数据库, 当通过Code First模式时,在第1次生成数据库时,则也通过下行执行迁移和更新命令行的结合数据库连接字符串,在(SQL Server)数据库软件中生成指定的数据库数据库、表、字段和约束规则。
Add-Migration Initialize(Initialize:自动生成的迁移类名,这里特指:20220803125612_Initialize.cs):
Update-Database Initialize(通过自动生成的迁移类中的定义,自动在指定的数据库软件中生成指定的数据库、表、字段和约束规则)
*/
}
#endregion
#region 属性
/// <summary>
/// 【学生】
/// <remarks>
/// 摘要:
/// 获取/设置学生实体的数据库设置实例,用于实现当前程序【Json】.【领域】.【学生集】.【学生】实体与“[JsonTable].[Student]”表之间的CURD的交互操作,并把这些数据存储到数据库设置实例中(内存)。
/// </remarks>
/// </summary>
public DbSet<Student> Student { get; set; }
/// <summary>
/// 【计划任务数据库设置】
/// <remarks>
/// 摘要:
/// 获取/设置计划任务实体的数据库设置实例,实现当前程序实现当前程序【Json】.【领域】.【计划任务集】.【计划任务】实体与“[JsonTable].[ScheduleTask]”表之间的CURD的交互操作,并把这些数据存储到数据库设置实例中(内存)。
/// </remarks>
/// </summary>
public DbSet<ScheduleTask> ScheduleTaskDbSet { get; set; }
#endregion
#region 方法--私有/保护--覆写
///<param name="builder">模型生成器实例,用于把当前程序中实体和属性所定义的约束规则,映射到数据库指定表及其字段上。</param>
/// <summary>
/// 【模型生成执行...】
/// <remarks>
/// 摘要:
/// 该方法把当前程序中实体和属性所定义的约束规则,映射到数据库指定表及其字段上。
/// </remarks>
/// </summary>
protected override void OnModelCreating(ModelBuilder builder)
{
//学生表主键约束规则,映射定义。
builder.Entity<Student>().HasKey(student => student.Id);
//计划任务表表名约束规则,映射定义。
builder.Entity<ScheduleTask>().ToTable(nameof(ScheduleTask));
//计划任务表主键约束规则,映射定义。
builder.Entity<ScheduleTask>().HasKey(scheduleTask => scheduleTask.Id);
base.OnModelCreating(builder);
}
#endregion
}
按F5执行程序后,打开SQL Server数据库软件中的JSONTable数据库,然后对下面的语句执行数据插入操作把持久连接计划任务实例的初始数据插入到[ScheduleTask]表中。
INSERT INTO [ScheduleTask] ([Name], [Type], [Seconds], [LastEnabled], [Enabled], [StopOnError], [LastStart], [LastEnd], [LastSuccess]) VALUES (N'Keep alive', N'JsonTable.Nop.Services.Common.KeepAliveTask, JsonTable', 300, CAST(N'2022-06-28T11:26:23.0000000' AS DateTime2), 1, 0, CAST(N'2022-08-21T07:18:41.0000000' AS DateTime2), CAST(N'2022-08-21T06:53:40.0000000' AS DateTime2), CAST(N'2022-08-21T06:53:40.0000000' AS DateTime2))
3 持久连接 (KeepAliveTask) 计划任务、第3方服务(StoreHttpClient)和持久连控制器(KeepAliveController)
持久连接 (KeepAliveTask) 计划任务、第3方服务(StoreHttpClient)和持久连控制器3者紧密耦合(集成)定义,构成了完整持久连接 (KeepAliveTask) 计划任务第3方服务
注意:
在nopCommerce程序中所有具体的计划任务具体实现类都必须继承于同1个接口“IScheduleTask”
1 IScheduleTask接口
/// <summary>
/// 【计划任务--接口】
/// <remarks>
/// 摘要:
/// 通过该类的成员方法法执行(持久)连接异步移步方法,获取执行的任务(Task)状态结果来确认(持久)连接状态。
/// </remarks>
/// </summary>
public partial interface IScheduleTask
{
#region 方法
/// <summary>
/// 【执行异步】
/// <remarks>
/// 摘要:
/// 该方法执行指定计划任务的异步移步方法,获取异步方法执行的任务(Task)状态结果来确认指定计划任务的执行状态。
/// </remarks>
/// </summary>
Task ExecuteAsync();
#endregion
}
2 第3方服务(StoreHttpClient) 的定义实现
// <summary>
/// 【网店HTTP客户端--类】
/// <remarks>
/// 摘要:
/// 通过该类的成员方法把当前程序作为当前程序的第3方服务,从而获取指定的客户端是否正在保持与当前程序(持久)连接状态中…。。
/// </remarks>
/// </summary>
public partial class StoreHttpClient
{
#region 变量--私有/保护
private readonly HttpClient _httpClient;
#endregion
#region 拷贝构造方法
public StoreHttpClient(HttpClient client)
{
//通过“HttpClient”实例,把当前程序实例化当前程序的第3方服务。
client.BaseAddress = new Uri("https://localhost:7006/");
_httpClient = client;
}
#endregion
#region 方法
/// <summary>
/// 【异步持久连接】
/// <remarks>
/// 摘要:
/// 该方法把当前程序作为当前程序的第3方服务,从而获取指定的客户端是否正在保持与当前程序(持久)连接状态中…;通过异步方法的任务(Task)状态结果来确认(持久)连接状态。
/// </remarks>
/// </summary>
public virtual async Task KeepAliveAsync()
{
await _httpClient.GetStringAsync("keepalive/index");
}
#endregion
}
注意:
StoreHttpClient必须注入到.Net(Core)框架的默认依赖注入容器中:
// 把当前程序定义的第3方持久化连接服务注入到.Net(Core)框架的默认依赖注入容器中。
builder.Services.AddHttpClient<StoreHttpClient>();
3 KeepAliveTask具体实现类
// <summary>
/// 【网店HTTP客户端--类】
/// <remarks>
/// 摘要:
/// 通过该类的成员方法法执行(持久)连接异步移步方法,获取执行的任务(Task)状态结果来确认(持久)连接状态。
/// </remarks>
/// </summary>
public partial class KeepAliveTask : IScheduleTask
{
#region 变量--私有/保护
private readonly StoreHttpClient _storeHttpClient;
#endregion
#region 拷贝构造方法
public KeepAliveTask(StoreHttpClient storeHttpClient)
{
_storeHttpClient = storeHttpClient;
}
#endregion
#region 方法
/// <summary>
/// 【执行异步】
/// <remarks>
/// 摘要:
/// 该方法执行(持久)连接异步移步方法,获取异步方法执行的任务(Task)状态结果来确认(持久)连接状态。
/// </remarks>
/// </summary>
public async System.Threading.Tasks.Task ExecuteAsync()
{
//执行(持久)连接异步移步方法,获取异步方法执行的任务(Task)状态结果来确认(持久)连接状态。
await _storeHttpClient.KeepAliveAsync();
}
#endregion
}
4 持久连控制器(KeepAliveController)
public class KeepAliveController : Controller
{
public virtual IActionResult Index()
{
return Content("第3方持久化连接服务正在保持与当前程序(持久)连接中…");
}
}
按F5执行程序后,输入https://localhost:7006/KeepAlive/,当前程序已经可以正常调用第3方持久化连接服务了
4 计划任务执行定义
1 IScheduleTaskRunner接口定义实现
///<summary>
/// 【计划任务运行器--接口】
/// <remarks>
/// 摘要:
/// 继承于该接口具体实现类的成员方法,对指定的计划任务实例进行执行操作。
/// </remarks>
/// </summary>
public interface IScheduleTaskRunner
{
/// <param name="scheduleTask">计划任务实体的1个指定实例。</param>
/// <param name="forceRun">指示指定的计划任务实例是否正在执行过程中…,默认值:false,即指定的计划任务实例未在执行过程中…。</param>
/// <param name="throwException">指示当前方法执行过程中产生异常时是否执行异常信息抛出操作,默认值:false,即当前程序不执行异常信息抛出操作。</param>
/// <param name="ensureRunOncePerPeriod">指示在指定的时间间隔(运行周期)内1个指定的计划任务实例只运行一次,默认值:true,即在指定的时间间隔(运行周期)内1个指定的计划任务实例只运行一次。</param>
/// <summary>
/// 【执行异步】
/// <remarks>
/// 摘要:
/// 通过相应的参数实例,获取1个值false(未作好准备)/true(作好准备),该值指定的计划任务是否已经作好了触发执行前的准备工作。
/// </remarks>
/// </summary>
Task ExecuteAsync(ScheduleTask scheduleTask, bool forceRun = false, bool throwException = false, bool ensureRunOncePerPeriod = true);
}
2 ScheduleTaskController定义实现
public class ScheduleTaskController : Controller
{
#region 变量--私有/保护
private readonly EFCoreContext _context;
private readonly IScheduleTaskRunner _taskRunner;
#endregion
#region 拷贝构造方法
public ScheduleTaskController(EFCoreContext context, IScheduleTaskRunner taskRunner)
{
_context = context;
_taskRunner = taskRunner;
}
#endregion
#region 方法
/// <param name="taskType">指定计划任务实例的类型名字符串实例。</param>
/// <summary>
/// 【运行任务】
/// <remarks>
/// 摘要:
/// 程序开始执行之后的期间内,保证定时器(Timer)实例,在指定的时间间隔之间,自动触发执行1个指定计划任务实例。
/// </remarks>
/// </summary>
[HttpPost]
public virtual async Task<IActionResult> RunTask(string taskType)
{
//根据参数实例,从计划任务表中获取1个指定计划任务实例。
var scheduleTask = await _context.ScheduleTaskDbSet.FirstOrDefaultAsync(s => s.Type.Equals(taskType));
//如果不存在计划任务实例,则直接退出当前方法。
if (scheduleTask == null)
return NoContent();
//执行【计划任务运行器】实例中的成员方法,用于通过定时器(Timer)实例,在定的时间间隔之间,自动触发执行1个指定计划任务实例。
await _taskRunner.ExecuteAsync(scheduleTask);
return NoContent();
}
#endregion
}
3 ScheduleTaskRunner具体实现类定义实现
///<summary>
/// 【计划任务运行器--类】
/// <remarks>
/// 摘要:
/// 通过该类的成员方法,对指定的计划任务实例进行执行操作。
/// </remarks>
/// </summary>
public partial class ScheduleTaskRunner : IScheduleTaskRunner
{
#region 变量--私有/保护
private readonly EFCoreContext _context;
private readonly IServiceScopeFactory _serviceScopeFactory;
#endregion
#region 拷贝构造方法
public ScheduleTaskRunner(IServiceScopeFactory serviceScopeFactory)
{
_serviceScopeFactory = serviceScopeFactory;
_context = _serviceScopeFactory.CreateScope().ServiceProvider.GetService<EFCoreContext>();
}
#endregion
#region 方法--私有/保护
/// <param name="scheduleTask">计划任务实体的1个指定实例。</param>
/// <summary>
/// 【执行任务】
/// <remarks>
/// 摘要:
/// 当前程序自动触发执行指定的计划任务时,该方法把指定的计划任务实例中的最新数据持久化更新到计划任务表的指定行的指定字段中。
/// </remarks>
/// </summary>
protected void ExecuteTask(ScheduleTask scheduleTask)
{
//获取指定计划任务的具体实现类的类型实例,如果没有加载指定计划任务的具体实现类的类型实例,则从当前程序的应用域中获取指定计划任务的具体实现类的类型实例。
var type = Type.GetType(scheduleTask.Type) ??
//从当前程序的应用域中获取指定计划任务的具体实现类的类型实例。
AppDomain.CurrentDomain.GetAssemblies()
.Select(a => a.GetType(scheduleTask.Type))
.FirstOrDefault(t => t != null);
if (type == null)
throw new Exception($"当前程序中没有定义该计划任务: ({scheduleTask.Type})。");
object instance = null;
try
{
//通过指定计划任务的具体实现类的类型实例,获取该类的实例,由于“IScheduleTask”接口及其具体实现类并没有注入到.Net(Core)默认依赖注入容器中,所以这些具体实现类的实例为:null。
instance = _serviceScopeFactory.CreateScope().ServiceProvider.GetService(type);
}
catch
{
}
//如果一个指定具体实现类没有注入到.Net(Core)默认依赖注入容器中,则必须通过调用该类的拷贝构造方法获取该类的实例。
if (instance == null)
{
Exception innerException = null;
foreach (var constructor in type.GetConstructors())
{
try
{
//依次获取指类的拷贝构造方法中所有参数的参数实例。
var parameters = constructor.GetParameters().Select(parameter =>
{
//获取指类的拷贝构造方法中指定参数的参数实例。
var service = _serviceScopeFactory.CreateScope().ServiceProvider.GetService(parameter.ParameterType);
if (service == null)
throw new Exception($"当前程序中没有定义该计划任务: ({scheduleTask.Type})。");
return service;
});
//通过“Activator.CreateInstance”方法获取个指定具体实现类的实例
instance = Activator.CreateInstance(type, parameters.ToArray());
}
catch (Exception ex)
{
innerException = ex;
}
}
}
//验证当前具体实现类的实例,是否由继承于“IScheduleTask”接口,如果不是,则直接退出当前方法。
if (instance is not IScheduleTask task)
return;
scheduleTask.LastStart = DateTime.Now;
//更新计划任务表中的【最后开始日期时间】字段中的时间为当前时间。
_context.ScheduleTaskDbSet.Update(scheduleTask);
_context.SaveChanges();
//调用指定的计划任务中的【执行异步】方法(这里特指:KeepAliveTask.ExecuteAsync【执行异步】方法),这里必须调用“Wait()”阻塞方法,以使异步方法的实现中保证:【最后结束日期时间】=【最后开始日期时间】+【间隔时间(秒)】。
task.ExecuteAsync().Wait();
scheduleTask.LastEnd = scheduleTask.LastSuccess = DateTime.Now;
//在指定的计划任务阻塞方法执行后,更新计划任务表中的【最后结束日期时间】字段中的时间为当前时间。
_context.ScheduleTaskDbSet.Update(scheduleTask);
_context.SaveChanges();
}
/// <param name="scheduleTask">计划任务实体的1个指定实例。</param>
/// <summary>
/// 【任务触发准备?】
/// <remarks>
/// 摘要:
/// 通过相应的参数实例,获取1个值false(未作好准备)/true(作好准备),该值指示指定的计划任务是否已经作好了触发执行前的准备工作。
/// </remarks>
/// <returns>
/// 返回:
/// 1个值falsefalse(未作好准备)/true(作好准备)。
/// </returns>
/// </summary>
protected virtual bool IsTaskAlreadyRunning(ScheduleTask scheduleTask)
{
//如果1个指定的计划任务的实例中没有相应的【最后开始日期时间】和【最后结束日期时间】,则该计划任务未作好触发执行前的准备工作。
if (!scheduleTask.LastStart.HasValue && !scheduleTask.LastEnd.HasValue)
return false;
//设定【最后开始日期时间】为当前最新时间。
var lastStart = scheduleTask.LastStart ?? DateTime.Now;
//如果1个指定的计划任务实例中有相应的【最后结束日期时间】,但【最后开始日期时间】小于【最后结束日期时间】,则该计划任务未作好触发执行前的准备工作:指定的计划任务已经触发执行过。
if (scheduleTask.LastEnd.HasValue && lastStart < scheduleTask.LastEnd)
return false;
//如果1个指定的计划任务实例中有相应的【最后开始日期时间】+【间隔时间(秒)】<=当前最新时间,则当该计划任务未作好触发执行前的准备工作:指定的计划任务正在触发执行过程中…;或已经触发执行过。
if (lastStart.AddSeconds(scheduleTask.Seconds) <= DateTime.Now)
return false;
return true;
}
#endregion
#region 方法
/// <param name="scheduleTask">计划任务实体的1个指定实例。</param>
/// <param name="forceRun">指示指定的计划任务实例是否正在执行过程中…,默认值:false,即指定的计划任务实例未在执行过程中…。</param>
/// <param name="throwException">指示当前方法执行过程中产生异常时是否执行异常信息抛出操作,默认值:false,即当前程序不执行异常信息抛出操作。</param>
/// <param name="ensureRunOncePerPeriod">指示在指定的时间间隔(运行周期)内1个指定的计划任务实例只运行一次,默认值:true,即在指定的时间间隔(运行周期)内1个指定的计划任务实例只运行一次。</param>
/// <summary>
/// 【执行异步】
/// <remarks>
/// 摘要:
/// 通过相应的参数实例,获取1个值false(未作好准备)/true(作好准备),该值指定的计划任务是否已经作好了触发执行前的准备工作。
/// </remarks>
/// </summary>
public async Task ExecuteAsync(ScheduleTask scheduleTask, bool forceRun = false, bool throwException = false, bool ensureRunOncePerPeriod = true)
{
var enabled = forceRun || (scheduleTask?.Enabled ?? false);
if (scheduleTask == null || !enabled)
return;
//保证在指定的时间间隔(运行周期)内1个指定的计划任务实例只运行一次。
if (ensureRunOncePerPeriod)
{
//如果指定的计划任务未作好触发执行前的准备工作,则直接退出当前方法。
if (IsTaskAlreadyRunning(scheduleTask))
return;
//如果指定的计划任务实例还没有执行结束(防止在指定计划任务实例的执行周期内,其他用户重复性的自动触发并执行该计划任务实例),则直接退出当前方法。
if (scheduleTask.LastStart.HasValue && (DateTime.Now - scheduleTask.LastStart).Value.TotalSeconds < scheduleTask.Seconds)
return;
}
try
{
//在指定的时间间隔和“300”秒之间获取其最小值,并把该最小值-1。
var expirationInSeconds = Math.Min(scheduleTask.Seconds, 300) - 1;
//以秒为单位,获取时间间隔,该时间间隔用于对锁定状态进行计时,而不是计划任务定时执行的时间间隔。
var expiration = TimeSpan.FromSeconds(expirationInSeconds);
//execute task with lock
// _locker.PerformActionWithLock(scheduleTask.Type, expiration, () => ExecuteTask(scheduleTask));
//调用当前类中的执行任务方法。
ExecuteTask(scheduleTask);
}
catch (Exception exc)
{
//如果指定的计划任务实例触发执行过程中产生异常,则当前程序自动禁用该计划任务,持久化更新计划任务表中指定行中的数据,及其把异常信息持久化到日志表中。
//var store = await _storeContext.GetCurrentStoreAsync();
//通过定时器自动触发并执行的行为方法的链接字符串。
var scheduleTaskUrl = $"https://localhost:7006/scheduletask/runtask";
scheduleTask.Enabled = !scheduleTask.StopOnError;
scheduleTask.LastEnd = DateTime.Now;
//在指定的计划任务阻塞方法执行后,更新计划任务表中的【启用?】字段中为:false;及其【最后结束日期时间】字段中的时间为当前时间。
_context.ScheduleTaskDbSet.Update(scheduleTask);
_context.SaveChanges();
//var message = string.Format(await _localizationService.GetResourceAsync("ScheduleTasks.Error"), scheduleTask.Name,
// exc.Message, scheduleTask.Type, store.Name, scheduleTaskUrl);
log error
//await _logger.ErrorAsync(message, exc);
if (throwException)
throw;
}
}
#endregion
}
5 定时器自动触发执行计划任务定义
1 ITaskScheduler接口
///<summary>
/// 【任务计划器--接口】
/// <remarks>
/// 摘要:
/// 通过继承该接口的具体实现类的成员方法成员,对计划任务表中的所有计划任务实例,并依次开辟(实例化/初始化)相应指定的线程实例,并存储到线程数组实例中,并通过定时器实例来自动执行线程数组实例中的线程实例,从而达到当前程序自动执行计划任务实例的目的。
/// </remarks>
/// </summary>
public interface ITaskScheduler
{
/// <summary>
/// 【异步初始化】
/// <remarks>
/// 摘要:
/// 该方法从主要作用是:为所有计划任务实例,依次开辟(实例化/初始化)相应指定的线程实例,并存储到线程数组实例中。
/// </remarks>
/// </summary>
Task InitializeAsync();
/// <summary>
/// 【开始运行计划器】
/// <remarks>
/// 摘要:
/// 对定时器实例化时,该方法将会以委托回调方法的形式作为相应的参数实例,该方法的主要作用是:通过定时器实例自动触发并依次开始执行当前程序中的所有计划任务实例。
/// </remarks>
/// </summary>
public void StartScheduler();
/// <summary>
/// 【停止运行计划器】
/// <remarks>
/// 摘要:
/// 对定时器实例化时,该方法将会以委托回调方法的形式作为相应的参数实例,该方法的主要作用是:依次停止当前程序中所有计划任务实例的线程实例,并通过系统方法释放计划任务实例的线程实例所占居的内存。
/// </remarks>
/// </summary>
public void StopScheduler();
}
2 TaskScheduler具体实现类
///<summary>
/// 【任务计划器--类】
/// <remarks>
/// 摘要:
/// 通过该类的成员方法成员,获取计划任务表中的所有计划任务实例,并依次开辟(实例化/初始化)相应指定的线程实例,并存储到线程数组实例中,并通过定时器实例来自动执行线程数组实例中的线程实例,从而达到当前程序自动执行计划任务实例的目的。
/// </remarks>
/// </summary>
public partial class TaskScheduler : ITaskScheduler
{
#region 变量--私有/保护
protected static readonly List<TaskThread> _taskThreads = new();
private readonly EFCoreContext _context;
#endregion
#region 拷贝构造方法
public TaskScheduler(IHttpClientFactory httpClientFactory,
IServiceScopeFactory serviceScopeFactory)
{
_context = serviceScopeFactory.CreateScope().ServiceProvider.GetService<EFCoreContext>();
TaskThread.HttpClientFactory = httpClientFactory;
TaskThread.ServiceScopeFactory = serviceScopeFactory;
}
#endregion
#region 方法
/// <summary>
/// 【异步初始化】
/// <remarks>
/// 摘要:
/// 该方法从主要作用是:为所有计划任务实例,依次开辟(实例化/初始化)相应指定的线程实例,并存储到线程数组实例中。
/// </remarks>
/// </summary>
public async Task InitializeAsync()
{
if (_taskThreads.Any())
return;
//通过异步方法,从计划任务表中获取所有的计划任务实例。
var scheduleTasks = await _context.ScheduleTaskDbSet.OrderBy(x => x.Seconds).ToListAsync();
var scheduleTaskUrl = $"https://localhost:7006/scheduletask/runtask";
//获取线程实例执行之间的时间间隔,以通过当前程序定义的"default"第3方服务,自动渡过这些时间。
int? timeout = null;
//把计划任务表中的所有计划任务实例,依次开辟(实例化/初始化)相应指定的线程实例,并存储到线程数组实例中。
foreach (var scheduleTask in scheduleTasks)
{
//为1个指定的计划任务实例,开辟(实例化/初始化)1个指定的线程实例。
var taskThread = new TaskThread(scheduleTask, scheduleTaskUrl, timeout)
{
Seconds = scheduleTask.Seconds
};
//为1个指定的线程实例,设定开始执行时间(如果程序重启或长时间不执行,该开始时间将被重新设定)。
if (scheduleTask.LastStart.HasValue)
{
//获取1个指定计划任务实例已经执行过的时间值。
var secondsLeft = (DateTime.Now - scheduleTask.LastStart).Value.TotalSeconds;
//如果1个指定计划任务实例,还没有执行,则设定执行1个指定线程实例所需的时间间隔(秒):0。
if (secondsLeft >= scheduleTask.Seconds)
taskThread.InitSeconds = 0;
else
//如果1个指定计划任务实例,已经执行过1段时间,则设定执行1个指定线程实例所需的时间间隔(秒)为:指定计划任务实例执行的剩余时间+1。
taskThread.InitSeconds = (int)(scheduleTask.Seconds - secondsLeft) + 1;
}
else if (scheduleTask.LastEnabled.HasValue)
{
//获取1个指定计划任务实例已经执行过的时间值。
var secondsLeft = (DateTime.Now - scheduleTask.LastEnabled).Value.TotalSeconds;
//如果1个指定计划任务实例,还没有执行,则设定执行1个指定线程实例所需的时间间隔(秒):0。
if (secondsLeft >= scheduleTask.Seconds)
taskThread.InitSeconds = 0;
else
//如果1个指定计划任务实例,已经执行过1段时间,则执行1个指定线程实例所需的时间间隔(秒):指定计划任务实例执行的剩余时间+1。
taskThread.InitSeconds = (int)(scheduleTask.Seconds - secondsLeft) + 1;
}
else
//当前程序第1次启动时,设定执行1个指定线程实例所需的时间间隔(秒)为:1个指定计划任务实例的【间隔时间(秒)】。
taskThread.InitSeconds = scheduleTask.Seconds;
//把1个指定线程实例,存储到线程数组实例中。
_taskThreads.Add(taskThread);
}
}
/// <summary>
/// 【开始运行计划器】
/// <remarks>
/// 摘要:
/// 对定时器实例化时,该方法将会以委托回调方法的形式作为相应的参数实例,该方法的主要作用是:通过定时器实例自动触发并依次开始执行当前程序中的所有计划任务实例。
/// </remarks>
/// </summary>
public void StartScheduler()
{
foreach (var taskThread in _taskThreads)
taskThread.InitTimer();
}
/// <summary>
/// 【停止运行计划器】
/// <remarks>
/// 摘要:
/// 对定时器实例化时,该方法将会以委托回调方法的形式作为相应的参数实例,该方法的主要作用是:依次停止当前程序中所有计划任务实例的线程实例,并通过系统方法释放计划任务实例的线程实例所占居的内存。
/// </remarks>
/// </summary>
public void StopScheduler()
{
foreach (var taskThread in _taskThreads)
taskThread.Dispose();
}
#endregion
}
3 TaskThread嵌套类
#region 嵌套类
///<summary>
/// 【任务线程--类】
/// <remarks>
/// 摘要:
/// 通过该类的成员方法,为指定的计划任务实例构建相应的线程实例,最后使用定时器实例自动触发运行线程实例,从而达到当前程序自动执行计划任务实例的目的。
/// </remarks>
/// </summary>
protected class TaskThread : IDisposable
{
#region 变量--私有/保护
protected readonly string _scheduleTaskUrl;
protected readonly ScheduleTask _scheduleTask;
protected readonly int? _timeout;
protected Timer _timer;
protected bool _disposed;
internal static IHttpClientFactory HttpClientFactory { get; set; }
internal static IServiceScopeFactory ServiceScopeFactory { get; set; }
#endregion
#region 拷贝构造方法
public TaskThread(ScheduleTask task, string scheduleTaskUrl, int? timeout)
{
_scheduleTaskUrl = scheduleTaskUrl;
_scheduleTask = task;
_timeout = timeout;
Seconds = 10 * 60;
}
#endregion
#region 属性
/// <summary>
/// 【间隔时间(秒)】
/// <remarks>
/// 摘要:
/// 获取/设置当前程序通过定时器方法自动触发执行同1个指定计划任务实例的时间间隔整型值(单位:秒)。
/// </remarks>
/// </summary>
public int Seconds { get; set; }
/// <summary>
/// 【初始间隔时间(秒)】
/// <remarks>
/// 摘要:
/// 获取/设置1指定线程实例之间所需的时间间隔(秒)。
/// </remarks>
/// </summary>
public int InitSeconds { get; set; }
/// <summary>
/// 【开始日期时间】
/// <remarks>
/// 摘要:
/// 获取/设置定时器实例自动触发执行1个计划任务实例的线程实例的开始时间。
/// </remarks>
/// </summary>
public DateTime StartedUtc { get; private set; }
/// <summary>
/// 【运行?】
/// <remarks>
/// 摘要:
/// 获取/设置1个值false(未运行)/true(运行),该值指示通过定时器实例自动触发的1个指定计划任务实例是否正在运行执行。
/// </remarks>
/// </summary>
public bool IsRunning { get; private set; }
/// <summary>
/// 【间隔时间(毫秒)】
/// <remarks>
/// 摘要:
/// 获取/设置定时器实例自动调用执行1个指定线程实例所需的时间间隔(毫秒),该时间间隔一般等于1个指定计划任务实例的执行时间的时间间隔(秒)* 1000(毫秒)。
/// </remarks>
/// </summary>
public int Interval
{
get
{
//把1个指定计划任务实例的执行时间的时间间隔(秒) * 1000(毫秒),该值不能大于最大整型(“int.MaxValue”)值:“ 2147483”。
var interval = Seconds * 1000;
if (interval <= 0)
interval = int.MaxValue;
return interval;
}
}
/// <summary>
/// 【初始间隔时间(毫秒)】
/// <remarks>
/// 摘要:
/// 获取/设置定时器实例自动调用执行1个指定线程实例所需的时间间隔(秒) * 1000(毫秒)。
/// </remarks>
/// </summary>
public int InitInterval
{
get
{
//把1个指定线程实例所需的时间间隔(秒) * 1000(毫秒),该值不能大于最大整型(“int.MaxValue”)值:“ 2147483”。
var interval = InitSeconds * 1000;
if (interval <= 0)
interval = 0;
return interval;
}
}
/// <summary>
/// 【只运行1次?】
/// <remarks>
/// 摘要:
/// 获取/设置1个值false(多次执行运行)/true(只执行运行1次),该值指示1个指定计划任务实例是否中在当前程序第1次启动时执行运行1次,如果不是,则由定时器实例自动触发并多次执行运行。
/// </remarks>
/// </summary>
public bool RunOnlyOnce { get; set; }
/// <summary>
/// 【开始?】
/// <remarks>
/// 摘要:
/// 获取/设置1个值false(未存储)/true(存储),该值指示内存中已经存储有了定时器非托管资源实例。
/// </remarks>
/// </summary>
public bool IsStarted => _timer != null;
/// <summary>
/// 【销毁?】
/// <remarks>
/// 摘要:
/// 获取/设置1个值false(未销毁)/true(销毁),该值指示定时器非托管资源实例的销毁释放操作是否已经完成。
/// </remarks>
/// </summary>
public bool IsDisposed => _disposed;
#endregion
#region 方法--私有/保护
/// <param name="state">该委托回调方法要使用的参数实例,或者为 null。</param>
/// <summary>
/// 【异步运行】
/// <remarks>
/// 摘要:
/// 定时器实例化时,该方法将会以委托回调方法的形式作为相应的参数实例,该方法的主要作用是:通过定时器实例自动触发并执行当前程序中的计划任务实例。
/// </remarks>
/// </summary>
private async Task RunAsync()
{
if (Seconds <= 0)
return;
StartedUtc = DateTime.Now;
//标记开始运行指定的计划任务实例。
IsRunning = true;
HttpClient client = null;
try
{
//如果任务线程(TaskThread)实例执行之间存在时间间隔,通过当前程序定义的"default"第3方服务,自动渡过这些时间,由于没有时间间隔所以本人没有定义依赖注入:services.AddHttpClient(NopHttpDefaults.DefaultHttpClient).WithProxy();。
client = HttpClientFactory.CreateClient("default");
if (_timeout.HasValue)
client.Timeout = TimeSpan.FromMilliseconds(_timeout.Value);
//获取“Post”方式调用“ScheduleTaskController.RunTask”方法中的参数实例。
var data = new FormUrlEncodedContent(new[] { new KeyValuePair<string, string>("taskType", _scheduleTask.Type) });
//通过客户端实例,以第3方服务的形式,并以“Post”方式调用“ScheduleTaskController.RunTask”方法。
await client.PostAsync(_scheduleTaskUrl, data);
}
catch (Exception ex)
{
//using var scope = ServiceScopeFactory.CreateScope();
// 把1个指定的计划任务的错误信息,持久化到数据库的日志表中。
//var logger = EngineContext.Current.Resolve<ILogger>(scope);
//var localizationService = EngineContext.Current.Resolve<ILocalizationService>(scope);
//var storeContext = EngineContext.Current.Resolve<IStoreContext>(scope);
//var message = ex.InnerException?.GetType() == typeof(TaskCanceledException) ? await localizationService.GetResourceAsync("ScheduleTasks.TimeoutError") : ex.Message;
//var store = await storeContext.GetCurrentStoreAsync();
//message = string.Format(await localizationService.GetResourceAsync("ScheduleTasks.Error"), _scheduleTask.Name,
// message, _scheduleTask.Type, store.Name, _scheduleTaskUrl);
//await logger.ErrorAsync(message, ex);
}
finally
{
client?.Dispose();
}
//标记结束运行指定的计划任务实例。
IsRunning = false;
}
/// <param name="state">该委托回调方法要使用的参数实例,或者为 null。</param>
/// <summary>
/// 【定时器句柄】
/// <remarks>
/// 摘要:
/// 定时器实例化时,该方法将会以委托回调方法的形式作为相应的参数实例,该方法的主要作用时通过定时器实例自动触发并执行当前程序中的计划任务实例。
/// </remarks>
/// </summary>
private void TimerHandler(object state)
{
try
{
//在程序启动后,在之后程序的执行期间,在定时器(Timer)实例”自动触发执行指定的计划任务实例时,通过设定参数实例“-1”,以“暂停”该“定时器(Timer)实例”的运行,以保证指定的计划任务实例得以完整的被执行。
_timer.Change(-1, -1);
//通过定时器实例自动触发并执行当前程序中的计划任务实例,注意必须使用“Wait()”阻塞方法,以保证定时器实例自动触发并执行当前程序中的计划任务实例,被完全执行后才能执行其它的异步方法。
RunAsync().Wait();
}
catch
{
}
finally
{
if (!_disposed && _timer != null)
{
//1个指定计划任务实例是否中在当前程序第1次启动时运行1次后,销毁并释放该指定计划任务实例所占有的内存。
if (RunOnlyOnce)
Dispose();
//如果不是则由定时器自动触发并多次执行运行1个指定计划任务实例。
else
//更改定时器时实例中TimerHandler委托回调方法的,延迟的时间(以毫秒为单位)和时间间隔(以毫秒为单位)。
//Interval--1:时器实例调用TimerHandler委托回调方法之前延迟的时间量(以毫秒为单位),即指定计划任务实例的时间间隔,这里特指持久连接计划任务实例的时间间隔初始值为:300000毫秒。
//Interval--2:定时器实例调用TimerHandler委托回调方法的时间间隔(以毫秒为单位),即指定计划任务实例的时间间隔,这里特指持久连接计划任务实例的时间间隔初始值为:300000毫秒。
_timer.Change(Interval, Interval);
}
}
}
#endregion
#region 方法--销毁
/// <summary>
/// 【销毁】
/// <remarks>
/// 摘要:
/// 通过系统方法“GC.SuppressFinalize”,执行定时器非托管资源实例的销毁释放操作,销毁释放操作完成后设定销毁变量“_disposed”为:true,即指定实例的销毁释放操作已经完成。
/// </remarks>
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <param name="disposing">指示否正在销毁释放定时器非托管资源实例所占有的内存,(隐式)默认值:false,即不销毁释放内存。</param>
/// <summary>
/// 【销毁】
/// <remarks>
/// 摘要:
/// 执行定时器非托管资源实例的销毁释放操作,销毁释放操作完成后设定销毁变量“_disposed”为:true,即指定实例的销毁释放操作已经完成。
/// </remarks>
/// </summary>
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
lock (this)
_timer?.Dispose();
_disposed = true;
}
#endregion
#region 方法
/// <summary>
/// 【初始化定时器】
/// <remarks>
/// 摘要:
/// 为当前类中的定时器变量实例,进行初始化/实例化设定。
/// </remarks>
/// </summary>
public void InitTimer()
{
//TimerHandler:委托回调方法。
//null:TimerHandler委托回调方法要使用的参数实例,该参数实例的默认值为:null。
//InitInterval:定时器实例调用TimerHandler委托回调方法之前延迟的时间量(以毫秒为单位),即所有计划任务实例的时间间隔,所有计划任务实例自动触发执行之间没有时间间隔初始值为:0。
//Interval:定时器实例调用TimerHandler委托回调方法的时间间隔(以毫秒为单位),即指定计划任务实例的时间间隔,这里特指持久连接计划任务实例的时间间隔初始值为:300000毫秒。
_timer ??= new Timer(TimerHandler, null, InitInterval, Interval);
}
#endregion
}
#endregion
6 其它
1 ApplicationBuilderExtensions
public static class ApplicationBuilderExtensions
{
public static bool IsDatabaseInstalled { get; set; }
/// <param name="application">.Net(Core)框架内置管道中间件实例。</param>
/// <summary>
/// 【开始执行引擎】
/// <remarks>
/// 摘要:
/// 保证证在程序开始执行时,实例化当前程序中的所有计划任务实例,并为这些任务实例构建相应的线程实例。
/// </remarks>
/// </summary>
public static void StartEngine(this IApplicationBuilder application)
{
if (IsDatabaseInstalled)
{
ITaskScheduler _taskScheduler = application.ApplicationServices.GetService<ITaskScheduler>();
_taskScheduler.InitializeAsync().Wait();
_taskScheduler.StartScheduler();
}
}
}
2 重构Program.cs
using JsonTable;
using JsonTable.Data;
using JsonTable.Nop.Services.Common;
using JsonTable.Nop.Services.ScheduleTasks;
using Microsoft.EntityFrameworkCore;
using ApplicationBuilderExtensions = JsonTable.ApplicationBuilderExtensions;
using TaskScheduler = JsonTable.Nop.Services.ScheduleTasks.TaskScheduler;
var builder = WebApplication.CreateBuilder(args);
//通过UseSqlServer依赖注入中间,通过“Windows身份认证”对SQL Server数据库进行身份验证,并与SQL Server数据库进行连接。
builder.Services.AddDbContext<EFCoreContext>
(options => options.UseSqlServer(builder.Configuration.GetConnectionString("SqlServerWindows")));
//通过UseSqlServer依赖注入中间,通过“SQL Server身份认证”对SQL Server数据库进行身份验证,并与SQL Server数据库进行连接,连接字符串中必须配置所连接数据库的:用户名、密码。
//builder.Services.AddDbContext<EFCoreContext>
// (options => options.UseSqlServer(builder.Configuration.GetConnectionString("SqlServerSQL")));
if (!string.IsNullOrEmpty(builder.Configuration.GetConnectionString("SqlServerWindows")))
{
ApplicationBuilderExtensions.IsDatabaseInstalled = true;
}
else
{
ApplicationBuilderExtensions.IsDatabaseInstalled = false;
}
// 把当前程序定义的第3方持久化连接服务注入到.Net(Core)框架的默认依赖注入容器中。
builder.Services.AddHttpClient<StoreHttpClient>();
builder.Services.AddTransient<IScheduleTaskRunner, ScheduleTaskRunner>();
builder.Services.AddSingleton<ITaskScheduler, TaskScheduler>();
//通过AddRazorRuntimeCompilation依赖注入中间件实现页面修改热加载(Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation)。
builder.Services
.AddControllersWithViews()
.AddRazorRuntimeCompilation();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
// Net(Core)内置管道中间件调用当前程序中所有计划任务(ScheduleTasks)实例,以保证在程序开始执行时,实例化当前程序中的所有计划任务实例,并为这些任务实例构建相应的线程实例;
// 同时保证在程序其后的执行过程中,定时器(Timer)实例,会在指定的时间间隔之间,自动出触发执行“ScheduleTaskController.RunTask”方法来实现计划任务实例的自动执行。
app.StartEngine();
app.Run();
注意:
本章中计划任务的源代码来源于:“nopCommerce_4.50.3”,而非“nopCommerce_4.40.4”,通过两个版本计划任务源代码对比,可以发现计划任务源代码被nopCommerce开发者进行了大量的重构。
对以上功能更为具体实现和注释见:22-08-25-065_JsonTable(nopCommerce计划任务(ScheduleTasks)的定义实现)