Asp.net core Webapi 如何执行定时任务?

发布时间 2023-12-26 17:04:29作者: 代码掌控者

image

前言

在计算机系统中,定时执行一些后台任务是很常见的场景,比如定时发送邮件、备份数据等等。

那么,.NET 技术如何通过编程灵活地实现项目里复杂的自定义任务呢?

如果是 Windows 生态,通常来说,可以有这些方式:

  1. 编写一个程序,通过 Windows 内置的任务计划来定时执行。
  2. 编写一个程序,通过 Windows 内置的 Services 来定时执行。
  3. 编写一个定时循环执行任务的程序,在 Windows 系统启动时配置为自动执行。
    ……

但是,如果是一个中小型的 Web 应用系统,这些方法方式就显得不太合适。Asp.net core Webapi 有没有办法执行定时任务呢?答案是有的,Asp.net core Webapi 可以通过常驻后台的托管服务来执行定时任务。

本文是 Asp.net core Webapi 运行一个常驻后台并从数据库中导出数据的托管服务的例子,写出来供大家指点,在讨论过程中共同提高水平。

Step By Step 实现步骤

  1. 创建一个 asp.net core webapi 项目
  2. 从 Nuget 安装以下包

    Microsoft.AspNetCore.Identity.EntityFrameworkCore
    Microsoft.EntityFrameworkCore.Relational
    Microsoft.EntityFrameworkCore.SqlServer
    Microsoft.EntityFrameworkCore.Tools

  3. 打开 appsettings.json 并添加数据库连接字符串,如:
    {
      "Logging": {
    	"LogLevel": {
    	  "Default": "Information",
    	  "Microsoft.AspNetCore": "Warning"
    	}
      },
      "AllowedHosts": "*",
      "ConnectionStrings": {
    	"Default": "Server=(localdb)\\mssqllocaldb;Database=IdentityTestDB;Trusted_Connection=True;MultipleActiveResultSets=true"
      }
    }
    
  4. 添加一个继承于 IdentityUser 的 User 类
    using Microsoft.AspNetCore.Identity;
    
    public class User: IdentityUser<long>
    {
    	public DateTime CreationTime { get; set; }
    	public string? NickName { get; set; }
    }	
    
  5. 添加一个继承于 IdentityRole 的 Role 类
    using Microsoft.AspNetCore.Identity;
    
    public class Role: IdentityRole<long>
    {
    
    }
    
  6. 创建数据库上下文
    using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
    using Microsoft.EntityFrameworkCore;
    
    public class TestDbContext: IdentityDbContext<User, Role, long>
    {
    	public TestDbContext(DbContextOptions<TestDbContext> options):base(options)
    	{
    
    	}
    
    	protected override void OnModelCreating(ModelBuilder builder)
    	{
    		base.OnModelCreating(builder);
    		builder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
    	}
    }	
    
  7. 创建一个 ExplortStatisticBgService 类并继承 BackgroundService,这是托管服务类
    using Microsoft.EntityFrameworkCore;
    using System.Text;
    
    public class ExplortStatisticBgService : BackgroundService
    {
    	private readonly TestDbContext ctx;
    	private readonly ILogger<ExplortStatisticBgService> logger;
    	private readonly IServiceScope serviceScope;
    
    	/// <summary>
    	/// 在构造方法注入IServiceScopeFactory服务,
    	/// 用来创建IServiceScope对象,
    	/// 这样就可以通过IServiceScope来创建短生命周期的服务了
    	/// </summary>
    	/// <param name="scopeFactory"></param>
    	public ExplortStatisticBgService(IServiceScopeFactory scopeFactory)
    	{
    		this.serviceScope = scopeFactory.CreateScope();
    		var sp = serviceScope.ServiceProvider;
    		this.ctx = sp.GetRequiredService<TestDbContext>();
    		this.logger = sp.GetRequiredService<ILogger<ExplortStatisticBgService>>();  
    	}
    
    	protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    	{
    		// 用 while 循环实现服务常驻
    		while (!stoppingToken.IsCancellationRequested)
    		{
    			// 用 try...catch 捕捉异常记录错误信息并避免方法退出
    			try
    			{
    				// 这里实现每隔5秒从数据库中导出数据
    				// 更复杂的配置可以用第三方开源的框架
    				await DoExecuteAsync();
    				await Task.Delay(5000);
    			}
    			catch (Exception ex)
    			{
    				logger.LogError(ex, "获取用户统计数据失败");
    				await Task.Delay(1000);
    			}
    		}
    	}
    
    	private async Task DoExecuteAsync()
    	{
    		var items = ctx.Users.AsNoTracking().GroupBy(u => u.CreationTime.Date)
    			.Select(e => new 
    			{ 
    				Date = e.Key,
    				Count = e.Count()
    			});
    		StringBuilder sb = new StringBuilder(1024);
    		sb.AppendLine($"Date: {DateTime.Now}");
    		foreach (var item in items)
    		{
    			sb.Append(item.Date).AppendLine($": {item.Count}");
    		}
    		await File.WriteAllTextAsync("d:/1.txt", sb.ToString());
    		logger.LogInformation($"导出完成");
    	}
    
    	/// <summary>
    	/// IServiceScope 需要释放
    	/// 所以重写 Dispose 方法
    	/// </summary>
    	public override void Dispose()
    	{
    		base.Dispose();
    		serviceScope.Dispose();
    	}
    }	
    
  8. 打开 Program.cs,注入托管服务等,看代码的注释
    using Microsoft.AspNetCore.Identity;
    using Microsoft.EntityFrameworkCore;
    
    var builder = WebApplication.CreateBuilder(args);
    
    // Add services to the container.
    
    builder.Services.AddControllers();
    // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
    builder.Services.AddEndpointsApiExplorer();
    builder.Services.AddSwaggerGen();
    
    IServiceCollection services = builder.Services;
    
    // 注册托管服务
    services.AddHostedService<ExplortStatisticBgService>();
    
    // 注入数据库上下文
    services.AddDbContext<TestDbContext>(options => {
    	string connStr = builder.Configuration.GetConnectionString("Default")!;
    	options.UseSqlServer(connStr);
    });
    
    // 数据保护服务注入
    // ----数据保护提供了一个简单、基于非对称加密改进的加密API用于确保Web应用敏感数据的安全存储
    // ----不需要开发人员自行生成密钥,它会根据当前应用的运行环境,生成该应用独有的一个私钥
    services.AddDataProtection();
    
    // 注入 Identity 框架的一些重要的基础配置
    // 如果没有这个,下面的注入 UserManager 等服务会有问题,程序无法编译
    services.AddIdentityCore<User>(options =>
    {
    	options.Password.RequireDigit = false;
    	options.Password.RequireLowercase = false;
    	options.Password.RequireNonAlphanumeric = false;
    	options.Password.RequireUppercase = false;
    	options.Password.RequiredLength = 6;
    	options.Tokens.PasswordResetTokenProvider = TokenOptions.DefaultEmailProvider;
    	options.Tokens.EmailConfirmationTokenProvider = TokenOptions.DefaultEmailProvider;
    });
    
    // 注入 UserManager、RoleManager 等Identity 框架服务
    var idBuilder = new IdentityBuilder(typeof(User), typeof(Role), services);
    idBuilder.AddEntityFrameworkStores<TestDbContext>()
    	.AddDefaultTokenProviders()
    	.AddRoleManager<RoleManager<Role>>()
    	.AddUserManager<UserManager<User>>();
    
    var app = builder.Build();
    
    // Configure the HTTP request pipeline.
    if (app.Environment.IsDevelopment())
    {
    	app.UseSwagger();
    	app.UseSwaggerUI();
    }
    
    app.UseHttpsRedirection();
    
    app.UseAuthorization();
    
    app.MapControllers();
    
    app.Run();
    ``
    
  9. Ctrl+F5 运行项目,不做任何操作,托管程序会自动导出数据

扩展

托管服务在后台运行,通过它可以实现在很多事情,比如:

  1. 监控消息队列,当有数据进入消息队列就处理。
  2. 再如每隔10s把A数据库中的数据同步到B数据库中
  3. ...... 等等