目錄html
咱們在作項目的時候, 每每要處理一些後臺的任務. 通常是兩種, 一種是不停的運行,好比消息隊列的消費者。另外一種是定時任務。linux
在.NET Framework + Windows環境裏, 咱們通常會使用 Windows 服務處理這些情形.git
但在.Net Core + Linux環境裏, 有沒有相似的解決方案呢? 瞭解的方法有兩種:github
建立一個控制檯程序並添加Hosting Nuget包。sql
Install-Package Microsoft.Extensions.Hosting Install-Package Microsoft.Extensions.Configuration.EnvironmentVariables Install-Package Microsoft.Extensions.Configuration.CommandLine Install-Package Microsoft.Extensions.Configuration.Json Install-Package Microsoft.Extensions.Logging.Console Install-Package Microsoft.Extensions.Logging.Debug
建立一個基於 Timer 的簡單 Hosted Service. 繼承自抽象類 BackgroundService.數據庫
public class TimedHostedService : BackgroundService { private readonly ILogger _logger; private Timer _timer; public TimedHostedService(ILogger<TimedHostedService> logger) { this._logger = logger; } protected override Task ExecuteAsync(CancellationToken stoppingToken) { _timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(5)); return Task.CompletedTask; } private void DoWork(object state) { _logger.LogInformation(string.Format("[{0:yyyy-MM-dd hh:mm:ss}] Timed Background Service is working.", DateTime.Now)); } public override Task StopAsync(CancellationToken cancellationToken) { _logger.LogInformation("Timed Background Service is stopping."); _timer?.Change(Timeout.Infinite, 0); return Task.CompletedTask; } public override void Dispose() { _timer?.Dispose(); base.Dispose(); } }
Main 函數中添加 Host的相關代碼.json
static async Task Main(string[] args) { var builder = new HostBuilder() //Host config .ConfigureHostConfiguration(configHost => { //配置根目錄 //configHost.SetBasePath(Directory.GetCurrentDirectory()); //讀取host的配置json,和appsetting相似 //configHost.AddJsonFile("hostsettings.json", true, true); //讀取環境變量,Asp.Net core默認的環境變量是以ASPNETCORE_做爲前綴的,這裏也採用此前綴以保持一致 configHost.AddEnvironmentVariables(prefix: "ASPNETCORE_"); //啓動host的時候之可傳入參數 if (args != null) { configHost.AddCommandLine(args); } }) //App config .ConfigureAppConfiguration((hostContext, configApp) => { //讀取應用的配置json configApp.AddJsonFile("appsettings.json", optional: true); //讀取應用特定環境下的配置json configApp.AddJsonFile($"appsettings.{hostContext.HostingEnvironment.EnvironmentName}.json", optional: true); //讀取環境變量 configApp.AddEnvironmentVariables(); //啓動host的時候可傳入參數 if (args != null) { configApp.AddCommandLine(args); } }) //配置服務及依賴注入註冊 .ConfigureServices((hostContext, services) => { //添加TimedHostedService services.AddHostedService<TimedHostedService>(); }) //日誌配置 .ConfigureLogging((hostContext, configLogging) => { //輸出控制檯日誌 configLogging.AddConsole(); //輸出Debug日誌 if (hostContext.HostingEnvironment.EnvironmentName == EnvironmentName.Development) { configLogging.AddDebug(); } }); //使用控制檯生命週期, Ctrl + C退出 await builder.RunConsoleAsync(); }
運行並測試.app
支持.Net Core的第三方日誌庫有不少, 下面以 Nlog 爲例, 集成到 Generic host 裏.框架
添加 NLog.Extensions.Hosting Nuget包async
Install-Package NLog.Extensions.Hosting
添加配置文件
新建一個文件nlog.config(建議所有小寫,linux系統中要注意), 並右鍵點擊其屬性,將其「複製到輸出目錄」設置爲「始終複製」。文件內容以下:
<?xml version="1.0" encoding="utf-8" ?> <nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" autoReload="true" internalLogLevel="info" internalLogFile="logs\internal-nlog.txt"> <!-- the targets to write to --> <targets> <!-- write logs to file --> <target xsi:type="File" name="fileTarget" fileName="logs\${shortdate}.log" layout="${date}|${level:uppercase=true}|${message} ${exception:format=tostring}|${logger}|${all-event-properties}" /> <target xsi:type="Console" name="consoleTarget" layout="${date}|${level:uppercase=true}|${message} ${exception:format=tostring}|${logger}|${all-event-properties}" /> </targets> <!-- rules to map from logger name to target --> <rules> <logger name="*" minlevel="Trace" writeTo="fileTarget,consoleTarget" /> </rules> </nlog>
<ItemGroup> <None Update="nlog.config"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </None> </ItemGroup>
修改 Main 方法:
static async Task Main(string[] args) { var logger = LogManager.GetCurrentClassLogger(); try { var builder = new HostBuilder() //Host config .ConfigureHostConfiguration(configHost => { }) //App config .ConfigureAppConfiguration((hostContext, configApp) => { }) //Services .ConfigureServices((hostContext, services) => { }) //Log //.ConfigureLogging((hostContext, configLogging) => //{ // configLogging.AddConsole(); // if (hostContext.HostingEnvironment.EnvironmentName == EnvironmentName.Development) // { // configLogging.AddDebug(); // } //}) .UseNLog(); await builder.RunConsoleAsync(); } catch (Exception ex) { logger.Fatal(ex, "Stopped program because of exception"); throw; } finally { // Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux) LogManager.Shutdown(); } }
運行並測試.
EF Core的集成相對比較簡單, 基本上和咱們在 MVC / WebAPI 中用法同樣.
添加 Nuget 包
// SQL Server Install-Package Microsoft.EntityFrameworkCore.SqlServer
添加實體
public class Product { public int Id { get; set; } public string Name { get; set; } public int Count { get; set; } } public class ProductConfiguration : IEntityTypeConfiguration<Product> { public void Configure(EntityTypeBuilder<Product> builder) { builder.Property(t => t.Name).IsRequired().HasMaxLength(100); builder.HasData(new Product { Id = 1, Name = "Test_Prouct_1", Count = 0 }); } }
添加數據庫上下文
public class HostDemoContext : DbContext { public HostDemoContext(DbContextOptions<HostDemoContext> options) : base(options) { } public DbSet<Product> Products { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.ApplyConfiguration(new ProductConfiguration()); } }
添加數據庫鏈接字符串到 appsettings.json 並修改 Main 方法
{ "sqlserverconnection": "Server=.\\SQLEXPRESS;Database=GenericHostDemo;Trusted_Connection=True;" }
//Services .ConfigureServices((hostContext, services) => { var connectionString = hostContext.Configuration["sqlserverconnection"]; services.AddDbContext<HostDemoContext>(o => o.UseSqlServer(connectionString), ServiceLifetime.Singleton); services.AddHostedService<TimedHostedService>(); })
在 Hosted Service 中使用 Context
private readonly HostDemoContext _context; public TimedHostedService(HostDemoContext context) { this._context = context; } private void DoWork(object state) { int id = 1; var product = _context.Products.Find(id); product.Count++; _context.SaveChanges(); _logger.LogInformation($"Processed {product.Name} at {DateTime.Now:yyyy-MM-dd hh:mm:ss}, current count is {product.Count}."); //_logger.LogInformation(string.Format("[{0:yyyy-MM-dd hh:mm:ss}] Timed Background Service is working.", DateTime.Now)); }
遷移Migration
Install-Package Microsoft.EntityFrameworkCore.Tools
建立 DesignTimeDbContextFactory 類
不一樣於Web項目, 執行Add-Migration遷移命令時候因爲拿不到鏈接字符串可能會報錯 Unable to create an object of type 'HostDemoContext'. Add an implementation of 'IDesignTimeDbContextFactory<HostDemoContext>' to the project, or see https://go.microsoft.com/fwlink/?linkid=851728 for additional patterns supported at design time.
解決方法:建立一個與DbContext同一目錄下的DesignTimeDbContextFactory文件,而後實現接口中的方法CreateDbContext,並配置ConnectionString:
public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<HostDemoContext> { public HostDemoContext CreateDbContext(string[] args) { var builder = new DbContextOptionsBuilder<HostDemoContext>(); builder.UseSqlServer("Server=.\\SQLEXPRESS;Database=GenericHostDemo;Trusted_Connection=True;"); return new HostDemoContext(builder.Options); } }
Add-Migration InitialCreate
更新遷移到數據庫. 執行命令 Update-Database
。
運行並測試.
接下來實現一個後臺任務用於監聽並消費 RabbitMQ 消息.
這裏使用庫EasyNetQ, 它是一款基於RabbitMQ.Client封裝的API庫,正如其名,使用起來比較Easy,它把原RabbitMQ.Client中的不少操做都進行了再次封裝,讓開發人員減小了不少工做量。詳細請參考 https://github.com/EasyNetQ/EasyNetQ
添加 EasyNetQ 相關 Nuget 包
install-package EasyNetQ install-package EasyNetQ.DI.Microsoft
建立後臺服務 RabbitMQDemoHostedService
public class RabbitMQDemoHostedService : BackgroundService { private readonly ILogger _logger; private readonly IBus _bus; public RabbitMQDemoHostedService(ILogger<RabbitMQDemoHostedService> logger, IBus bus) { this._logger = logger; this._bus = bus; } protected override Task ExecuteAsync(CancellationToken stoppingToken) { _bus.Subscribe<DemoMessage>("demo_subscription_1", HandleDemoMessage); return Task.CompletedTask; } private void HandleDemoMessage(DemoMessage demoMessage) { _logger.LogInformation($"Got Message : {demoMessage.Id} {demoMessage.Text} {demoMessage.CreatedTime}"); } }
Program 裏面配置相關服務
//Services .ConfigureServices((hostContext, services) => { //Rabbit MQ string rabbitMqConnection = hostContext.Configuration["rabbitmqconnection"]; services.RegisterEasyNetQ(rabbitMqConnection); services.AddHostedService<RabbitMQDemoHostedService>(); })
啓動 MQ 發送程序來模擬消息的發送並測試.
Quartz 是一個開源做業調度框架, quartznet的詳細信息請參考 http://www.quartz-scheduler.net/ , 集成 Quartz 能夠幫助提供比只使用定時器更強大的功能.
添加 Quartz 相關 Nuget 包
Install-Package Quartz Install-Package Quartz.Plugins
新建 Quartz 配置類 QuartzOption
// More settings refer to:https://github.com/quartznet/quartznet/blob/master/src/Quartz/Impl/StdSchedulerFactory.cs public class QuartzOption { public QuartzOption(IConfiguration config) { if (config == null) { throw new ArgumentNullException(nameof(config)); } var section = config.GetSection("quartz"); section.Bind(this); } public Scheduler Scheduler { get; set; } public ThreadPool ThreadPool { get; set; } public Plugin Plugin { get; set; } public NameValueCollection ToProperties() { var properties = new NameValueCollection { ["quartz.scheduler.instanceName"] = Scheduler?.InstanceName, ["quartz.threadPool.type"] = ThreadPool?.Type, ["quartz.threadPool.threadPriority"] = ThreadPool?.ThreadPriority, ["quartz.threadPool.threadCount"] = ThreadPool?.ThreadCount.ToString(), ["quartz.plugin.jobInitializer.type"] = Plugin?.JobInitializer?.Type, ["quartz.plugin.jobInitializer.fileNames"] = Plugin?.JobInitializer?.FileNames }; return properties; } } public class Scheduler { public string InstanceName { get; set; } } public class ThreadPool { public string Type { get; set; } public string ThreadPriority { get; set; } public int ThreadCount { get; set; } } public class Plugin { public JobInitializer JobInitializer { get; set; } } public class JobInitializer { public string Type { get; set; } public string FileNames { get; set; } }
重寫 JobFactory
public class JobFactory : IJobFactory { private readonly IServiceProvider _serviceProvider; public JobFactory(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler) { var job = _serviceProvider.GetService(bundle.JobDetail.JobType) as IJob; return job; } public void ReturnJob(IJob job) { var disposable = job as IDisposable; disposable?.Dispose(); } }
編寫 Quartz Hosted Service
public class QuartzService : BackgroundService { private readonly ILogger _logger; private readonly IScheduler _scheduler; public QuartzService(ILogger<QuartzService> logger, IScheduler scheduler) { _logger = logger; _scheduler = scheduler; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("Start quartz scheduler..."); await _scheduler.Start(stoppingToken); } public override async Task StopAsync(CancellationToken cancellationToken) { _logger.LogInformation("Stop quartz scheduler..."); await _scheduler.Shutdown(cancellationToken); await base.StopAsync(cancellationToken); } }
編寫一個測試 Job
public class TestJob : IJob { private readonly ILogger _logger; public TestJob(ILogger<TestJob> logger) { _logger = logger; } public Task Execute(IJobExecutionContext context) { _logger.LogInformation(string.Format("[{0:yyyy-MM-dd hh:mm:ss}] TestJob is working.", DateTime.Now)); return Task.CompletedTask; } }
準備 appsettings.json 文件
"quartz": { "scheduler": { "instanceName": "GenericHostDemo.Quartz" }, "threadPool": { "type": "Quartz.Simpl.SimpleThreadPool, Quartz", "threadPriority": "Normal", "threadCount": 10 }, "plugin": { "jobInitializer": { "type": "Quartz.Plugin.Xml.XMLSchedulingDataProcessorPlugin, Quartz.Plugins", "fileNames": "quartz_jobs.xml" } } }
準備 Quartz 的調度文件 quartz_jobs.xml, 並右鍵點擊其屬性,將其「複製到輸出目錄」設置爲「始終複製」。
<?xml version="1.0" encoding="UTF-8"?> <job-scheduling-data xmlns="http://quartznet.sourceforge.net/JobSchedulingData" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.0"> <processing-directives> <overwrite-existing-data>true</overwrite-existing-data> </processing-directives> <schedule> <job> <name>TestJob</name> <group>TestGroup</group> <description>TestJob Description</description> <job-type>GenericHostDemo.Jobs.TestJob, GenericHostDemo</job-type> <durable>true</durable> <recover>false</recover> </job> <trigger> <simple> <name>TestJobTrigger</name> <group>TestGroup</group> <description>TestJobTrigger Description</description> <job-name>TestJob</job-name> <job-group>TestGroup</job-group> <repeat-count>-1</repeat-count> <repeat-interval>5000</repeat-interval> </simple> </trigger> </schedule> </job-scheduling-data>
Program 註冊 Quartz Hosted Service 和 TestJob
//Quartz services.AddSingleton<IJobFactory, JobFactory>(); services.AddSingleton(provider => { var option = new QuartzOption(hostContext.Configuration); var sf = new StdSchedulerFactory(option.ToProperties()); var scheduler = sf.GetScheduler().Result; scheduler.JobFactory = provider.GetService<IJobFactory>(); return scheduler; }); services.AddHostedService<QuartzService>(); //Jobs services.AddSingleton<TestJob>();
運行並查看結果.
通用主機的部署其實就是讓它在後臺運行。
在Linux下面讓程序在後臺運行方式有不少種,好比經過systemctl, Supervisor等。
也能夠把它部署在Docker裏面.
在 .NET Core 2.1 以前, 須要本身實現 IHostedService 接口來建立服務. 在 .NET Core 2.1 中, 考慮到大多數後臺任務在取消令牌管理和其餘典型操做方面有類似的需求,微軟提供了一個很是方便的抽象基類BackgroundService. 源碼以下:
// 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 stoppingToken); 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(); } }