This is a viewer only at the moment see the article on how this works.
To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk
This is a preview from the server running through my markdig pipeline
Thursday, 27 November 2025
每一个现代网络应用程序都有工作,不能阻止 HTTP 发送电子邮件、处理文件、与外部服务同步、运行计划维护、运行计划维护。 ASP.NET Core 提供了处理这一背景工作的多种方法,从简单到简单 IHostedService 执行到复杂的框架, 比如“ 燃火” 。 在第一部分, 我们将探索基本模式, 以及何时使用每个模式 。
这个网站(BLOG站点!)有超过一半的人从事讽刺性的背景工作, 但和他们拥有的事物一样, 背景服务是现代网络应用程序的隐形英雄。 虽然您的控制器在前景中处理 HTTP 请求, 但背景服务会悄悄处理排队的电子邮件、 搜索索引内容、 检查外部 API 、 清理临时文件, 并处理无数其它任务, 否则会阻碍您的请求管道 。
在这个由两部分组成的系列系列中, 我们将探索不同方法 来实施 ASP.NET核心的背景服务, IHostedService 和 BackgroundService 在第一部分中,我们将研究基本方法及其特性。在第一部分中,我们将研究这些基本方法及其特性。 第二部分 第二部分,我们将潜入现实世界执行 从生产代码库。
重要: 我们将特别关注生命周期管理 特别是常被忽略的
StopAsync许多开发商在关闭应用程序时 遇到隐秘的例外。
在潜入“如何”之前,让我们简单考虑一下“为什么”。背景服务允许你:
ASP.NET核心提供几种执行这些服务的办法,每种办法的权衡取舍不同。
在“旧时”(2010年以前),在您的网络应用程序中运行背景工作通常被认为是一个坏主意。 常规智慧是 : “ 网络服务器处理网络请求。 背景工作属于一个单独的服务器 。 ”
这不仅仅是货物技术的智慧, 而是基于真正的技术限制:
早期网络服务器(现在诚实地说,“便宜”的Azure服务)通常使用 单一核心或双核心CPU。如果您执行CPU密集的背景任务,它与同一核心的网络请求直接竞争:
Single Core (2005):
┌─────────────────────┐
│ Background Task │ ← Uses 80% CPU
│ (80% of core) │
├─────────────────────┤
│ Web Requests │ ← Only 20% left!
│ (20% of core) │ ← Slow responses
└─────────────────────┘
结果:你的网站在背景工作启动的那一刻变得迟钝。
使用的经典ASP.NET 要求的线串线索库相对较小(通常是25-100线索),背景任务将窃取应处理网络请求的线索:
// Classic ASP.NET (2008)
ThreadPool.QueueUserWorkItem(_ =>
{
// This steals a thread from the pool!
ProcessLongRunningTask();
});
// Meanwhile, web requests are queued waiting for threads
// HTTP 503 Service Unavailable
IIS将根据内存限制、请求计数或时间表,积极回收应用池(重新启动应用软件)。
00:00 - Background import starts (2 hour task)
02:00 - IIS recycles app pool (scheduled)
- Background task killed
- Work lost, must start again
在.NET4.5(2012)之前,同步编程(相对而言)很痛苦,背景任务往往不必要地堵住线条:
// Pre-async (2008)
void ProcessEmails()
{
foreach (var email in GetEmails())
{
smtp.Send(email); // Blocks thread for 500ms per email
}
}
// 100 emails = 50 seconds of blocked thread time
今日的风景与众不同:
经济学已经翻转了。 含有多个核心的云层VM 价格合理,而光金属服务器却价格低得令人惊讶。 这个博客在专用的8核心服务器上运行,其成本低于可比的Azure VM — — 而且所有这些核心都交给我自己,没有吵闹的邻居。 一个核心的背景任务不会极大地影响网络对其他核心的要求:
8-Core Server (2024):
Core 1: ████████████████████ Web Requests
Core 2: ████████████████████ Web Requests
Core 3: ████████████████████ Web Requests
Core 4: ████████████████████ Web Requests
Core 5: ████████████████████ Background Task ← Isolated
Core 6: ████████████████████ Background Task
Core 7: ████████████████████ Background Task
Core 8: ████████████████████ Background Task
现代. NET 使非同步编程变得微不足道。 背景任务可以在 I/ O 上等待, 而不屏蔽线索 :
// Modern async (2024)
async Task ProcessEmailsAsync(CancellationToken ct)
{
await foreach (var email in GetEmailsAsync(ct))
{
await smtp.SendAsync(email, ct); // Doesn't block thread!
}
}
// 100 emails processed efficiently, thread returns to pool during I/O
NET现在有头等支持。 System.Threading.Channels:
// System.Threading.Channels
var channel = Channel.CreateBounded<Email>(100);
// Producer (web request)
await channel.Writer.WriteAsync(email); // Fast, non-blocking
// Consumer (background service)
await foreach (var email in channel.Reader.ReadAllAsync())
{
await ProcessAsync(email); // Efficient, async
}
现代集装箱管弦手让你 限制资源使用:
# Kubernetes resource limits
resources:
limits:
cpu: "500m" # Background task can't use more than 0.5 CPU
memory: "512Mi" # Or more than 512 MB RAM
这意味着一个离家出走的背景任务 不能饿死你的网络层。
问题不再是“我们能否在网络应用程序中提供背景服务?”**我们应该吗?**稍后我们会在“何时不使用背景服务”部分探讨这一决定。
ASP.NET核心的每一项背景服务的核心是其核心内容。 IHostedService。这个界面非常简单:
public interface IHostedService
{
Task StartAsync(CancellationToken cancellationToken);
Task StopAsync(CancellationToken cancellationToken);
}
就是这样 两种方法 StartAsync 您的应用程序启动时会被调用,并且 StopAsync 当它关闭的时候,
登记您在 Program.cs:
builder.Services.AddHostedService<MyBackgroundService>();
以下是生命周期的可视化:
graph LR
A[Application Starts] --> B[StartAsync Called]
B --> C[Service Running]
C --> D[Application Shutting Down]
D --> E[StopAsync Called]
E --> F[Application Stopped]
style A stroke:#059669,stroke-width:3px,color:#10b981
style C stroke:#2563eb,stroke-width:3px,color:#3b82f6
style F stroke:#dc2626,stroke-width:3px,color:#ef4444
执行时的一项关键决定 IHostedService 是否是您的 StartAsync 方法应该立即屏蔽或返回。
同步( 阻塞) 开始 :
public class BlockingStartService : IHostedService
{
public async Task StartAsync(CancellationToken cancellationToken)
{
// This blocks application startup until complete
await InitializeDatabaseAsync(cancellationToken);
await LoadConfigurationAsync(cancellationToken);
// Only now will the application continue starting
}
public Task StopAsync(CancellationToken cancellationToken)
=> Task.CompletedTask;
}
非同步( 非阻塞) 开始 :
public class NonBlockingStartService : IHostedService
{
private Task _backgroundTask;
private readonly CancellationTokenSource _cts = new();
public Task StartAsync(CancellationToken cancellationToken)
{
// Start background work but return immediately
_backgroundTask = Task.Run(async () =>
{
// Give other services time to initialise
await Task.Delay(TimeSpan.FromSeconds(5), _cts.Token);
await DoLongRunningWorkAsync(_cts.Token);
}, _cts.Token);
return Task.CompletedTask;
}
public async Task StopAsync(CancellationToken cancellationToken)
{
_cts.Cancel();
await _backgroundTask; // Wait for completion
}
}
何时使用每种方针:
这里就是事情变得有趣的地方 许多开发商遇到问题的地方。当你的应用程序关闭时, ASP.NET Core calls StopAsync 所有主机服务。 您有一个有限的窗口( 默认 5 秒) , 可以优雅地清理。 您可以在 Program.cs:
builder.Services.Configure<HostOptions>(options =>
{
options.ShutdownTimeout = TimeSpan.FromSeconds(30);
});
最常见的错误是:
public class BrokenService : IHostedService
{
private readonly Channel<string> _channel = Channel.CreateUnbounded<string>();
private Task _processingTask;
public Task StartAsync(CancellationToken cancellationToken)
{
_processingTask = ProcessMessagesAsync();
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
// WRONG: The channel is still open, ProcessMessagesAsync
// will hang on WaitToReadAsync forever!
return Task.CompletedTask;
}
private async Task ProcessMessagesAsync()
{
// This will never exit because the channel is never completed
await foreach (var message in _channel.Reader.ReadAllAsync())
{
await ProcessAsync(message);
}
}
}
当您运行此服务并停止您的应用程序时, 您将会看到错误, 比如 :
Unable to cast object of type 'TaskCompletionSource`1[System.Threading.Tasks.VoidTaskResult]' to type 'System.Threading.Tasks.Task'
或者申请在强制终止之前,只需暂停停产的暂停期。
正确的做法:
public class CorrectService : IHostedService
{
private readonly Channel<string> _channel = Channel.CreateUnbounded<string>();
private readonly CancellationTokenSource _cts = new();
private Task _processingTask;
public Task StartAsync(CancellationToken cancellationToken)
{
_processingTask = ProcessMessagesAsync(_cts.Token);
return Task.CompletedTask;
}
public async Task StopAsync(CancellationToken cancellationToken)
{
// CORRECT: Signal cancellation and complete the channel
await _cts.CancelAsync();
_channel.Writer.Complete();
try
{
// Wait for processing to finish or for the shutdown timeout
await Task.WhenAny(_processingTask,
Task.Delay(Timeout.Infinite, cancellationToken));
}
catch (OperationCanceledException)
{
// Expected when shutdown timeout is reached
}
}
private async Task ProcessMessagesAsync(CancellationToken token)
{
await foreach (var message in _channel.Reader.ReadAllAsync(token))
{
try
{
await ProcessAsync(message);
}
catch (OperationCanceledException)
{
// Shutdown requested, exit gracefully
break;
}
}
}
}
正确执行 StopAsync 的要点 :
CancellationTokenSource 并取消它Writer.Complete()Task.WhenAny 关闭取消代号StopAsync 可能造成不可预测行为书写 IHostedService 执行可以是重复的。您总是需要背景任务、取消符号源以及相同的清理模式。 BackgroundService 为你处理这个锅炉板:
public abstract class BackgroundService : IHostedService, IDisposable
{
private Task _executeTask;
private CancellationTokenSource _stoppingCts;
protected abstract Task ExecuteAsync(CancellationToken stoppingToken);
public virtual Task StartAsync(CancellationToken cancellationToken)
{
_stoppingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_executeTask = ExecuteAsync(_stoppingCts.Token);
return Task.CompletedTask;
}
public virtual async Task StopAsync(CancellationToken cancellationToken)
{
if (_executeTask == null) return;
try
{
_stoppingCts.Cancel();
}
finally
{
await Task.WhenAny(_executeTask, Task.Delay(Timeout.Infinite, cancellationToken));
}
}
public virtual void Dispose()
{
_stoppingCts?.Cancel();
}
}
你只要执行 ExecuteAsync 让基础班处理管道:
public class SimpleBackgroundService : BackgroundService
{
private readonly ILogger<SimpleBackgroundService> _logger;
public SimpleBackgroundService(ILogger<SimpleBackgroundService> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Service starting");
// Wait for app to finish starting
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
while (!stoppingToken.IsCancellationRequested)
{
try
{
await DoWorkAsync(stoppingToken);
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
}
catch (OperationCanceledException)
{
// Shutdown requested
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in background service");
}
}
_logger.LogInformation("Service stopping");
}
private async Task DoWorkAsync(CancellationToken token)
{
_logger.LogInformation("Doing work...");
// Your actual work here
await Task.Delay(1000, token);
}
}
使用使用 BackgroundService 时间:
StartAsync/StopAsync 时间时机使用使用 IHostedService 时间:
StartAsync 背景工作有时您需要服务等待对方。例如,您可能需要您的语义搜索索引, 等待您标记文件处理器完成初始负载 。
以下是一个协调服务启动的模式:
public interface IStartupCoordinator
{
void RegisterService(string serviceName);
void SignalReady(string serviceName);
bool IsServiceReady(string serviceName);
Task WaitForServiceAsync(string serviceName, CancellationToken cancellationToken = default);
Task WaitForAllServicesAsync(CancellationToken cancellationToken = default);
}
public class StartupCoordinator : IStartupCoordinator
{
private readonly ConcurrentDictionary<string, TaskCompletionSource> _services = new();
private readonly ILogger<StartupCoordinator> _logger;
public void RegisterService(string serviceName)
{
_services.TryAdd(serviceName, new TaskCompletionSource());
}
public void SignalReady(string serviceName)
{
if (_services.TryGetValue(serviceName, out var tcs))
{
tcs.TrySetResult();
_logger.LogInformation("{Service} is ready", serviceName);
}
}
public async Task WaitForServiceAsync(string serviceName, CancellationToken ct = default)
{
if (_services.TryGetValue(serviceName, out var tcs))
{
await tcs.Task.WaitAsync(ct);
}
}
public async Task WaitForAllServicesAsync(CancellationToken ct = default)
{
await Task.WhenAll(_services.Values.Select(tcs => tcs.Task)).WaitAsync(ct);
}
}
服务使用率 :
public class DependentService : IHostedService
{
private readonly IStartupCoordinator _coordinator;
private readonly ILogger<DependentService> _logger;
public DependentService(
IStartupCoordinator coordinator,
ILogger<DependentService> logger)
{
_coordinator = coordinator;
_logger = logger;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
// Wait for another service to be ready
await _coordinator.WaitForServiceAsync("MarkdownProcessor", cancellationToken);
_logger.LogInformation("Dependencies ready, starting work");
// Do your work...
// Signal you're ready for services that depend on you
_coordinator.SignalReady("DependentService");
}
public Task StopAsync(CancellationToken cancellationToken)
=> Task.CompletedTask;
}
当您拥有多种具有相互依存关系的背景服务时,这种模式变得特别有用。
启动协调员在一个应用程序实例中工作。 但是当您向多个实例扩展时会怎样? 您不希望三个实例同时运行相同的计划任务 。
重新重订 提供一个简单的解决办法:使用旗帜(钥匙)来协调谁做什么。
public class DistributedBackgroundService : BackgroundService
{
private readonly IConnectionMultiplexer _redis;
private readonly ILogger<DistributedBackgroundService> _logger;
private readonly string _instanceId = Guid.NewGuid().ToString();
private const string LeaderKey = "background:newsletter:leader";
public DistributedBackgroundService(
IConnectionMultiplexer redis,
ILogger<DistributedBackgroundService> logger)
{
_redis = redis;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var db = _redis.GetDatabase();
while (!stoppingToken.IsCancellationRequested)
{
// Try to become the leader (SET NX with expiry)
var acquired = await db.StringSetAsync(
LeaderKey,
_instanceId,
TimeSpan.FromMinutes(5),
When.NotExists);
if (acquired)
{
_logger.LogInformation("This instance is the leader, running task");
try
{
await DoScheduledWorkAsync(stoppingToken);
}
finally
{
// Release leadership
await db.KeyDeleteAsync(LeaderKey);
}
}
else
{
var leader = await db.StringGetAsync(LeaderKey);
_logger.LogDebug("Another instance ({Leader}) is the leader", leader);
}
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
}
}
}
对于不能同时执行的任务,不能同时执行:
public async Task ProcessWithLockAsync(CancellationToken cancellationToken)
{
var db = _redis.GetDatabase();
var lockKey = "locks:critical-task";
var lockValue = _instanceId;
// Try to acquire lock
if (await db.LockTakeAsync(lockKey, lockValue, TimeSpan.FromMinutes(10)))
{
try
{
_logger.LogInformation("Lock acquired, processing...");
await DoCriticalWorkAsync(cancellationToken);
}
finally
{
await db.LockReleaseAsync(lockKey, lockValue);
}
}
else
{
_logger.LogDebug("Could not acquire lock, another instance is processing");
}
}
对于更复杂的情景(多步骤工作、跨重新启动的可靠日程安排),请考虑使用数据库后端自动处理分布式锁定的Hangfire(Hangfire)。
在我们潜入更先进的工具之前, 比如杭火, 让我们来谈谈当你 不应该 在您的主网络应用程序中使用背景服务 。
您的网络应用程序中运行的背景服务与 HTTP 请求的管道共享资源。 这可能造成问题 :
问题: 您的背景服务消耗了大量 CPU、内存或数据库连接 。
// This will starve your web application
public class VideoTranscodingService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var video = await _queue.DequeueAsync();
// This uses 100% of 4 CPU cores for 5 minutes
await TranscodeVideoAsync(video);
}
}
}
当网络请求在转换编码期间到达时, 它们速度缓慢, 因为 CPU 忙碌。
解决方案 : 移动到单独的工人服务处 :
# Your solution structure
/YourApp.Web # ASP.NET Core web app - no background services
/YourApp.Worker # .NET Worker Service - handles background work
/YourApp.Shared # Shared models, interfaces
问题: 您的背景工作需要与网络水平不同的缩放 。
如果他们在同一个过程, 你不能独立地放大它们。
实例设想:
09:00 - High web traffic, low background work → Need 10 web instances, 1 worker
14:00 - Newsletter time! Low web traffic, high background work → Need 2 web instances, 20 workers
将背景服务放到网络应用程序中意味着你必须运行20个网络事件, 才能处理通讯,浪费资源。
问题: 您想要在不重新启动背景服务( 反之亦然) 的情况下部署 Web 更改 。
// If this is in your web app, deploying a CSS change restarts the service
public class LongRunningImportService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// This import takes 2 hours
await ImportMillionsOfRecordsAsync(stoppingToken);
}
}
每次部署都中断进口。 将其移动到您独立部署的单独的工人服务处 。
问题: 您背景服务中的一个错误崩溃了整个网络应用程序 。
// This null reference exception crashes your web app
public class BuggyBackgroundService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
string value = null;
// Unhandled exception - takes down the whole app
await ProcessAsync(value.Length);
}
}
如果背景工作是一个单独的进程,它可能崩溃和重新开始,而不影响网络请求。
当你决定分开时,这里是推荐的建筑:
使用 worker Service 模板创建新工程 :
dotnet new worker -n YourApp.Worker
结构:
/YourApp.Worker
/Services
VideoTranscodingService.cs
EmailSenderService.cs
/Program.cs
/appsettings.json
方案:
var builder = Host.CreateApplicationBuilder(args);
// Register your background services
builder.Services.AddHostedService<VideoTranscodingService>();
builder.Services.AddHostedService<EmailSenderService>();
// Share configuration with web app
builder.Services.Configure<VideoConfig>(
builder.Configuration.GetSection("Video"));
// Share database context
builder.Services.AddDbContext<YourDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
var host = builder.Build();
host.Run();
单独部署 :
# Web app on ports 80/443
/YourApp.Web → web-server-1, web-server-2, web-server-3
# Worker service doesn't listen on any port
/YourApp.Worker → worker-server-1, worker-server-2
使用消息队列来拆分 Web 和工人 :
graph LR
A[Web App] --> B[Message Queue]
B --> C[Worker 1]
B --> D[Worker 2]
B --> E[Worker N]
style A stroke:#059669,stroke-width:3px,color:#10b981
style B stroke:#2563eb,stroke-width:3px,color:#3b82f6
style C stroke:#7c3aed,stroke-width:3px,color:#8b5cf6
style D stroke:#7c3aed,stroke-width:3px,color:#8b5cf6
style E stroke:#7c3aed,stroke-width:3px,color:#8b5cf6
网络应用程序队列工作 :
// In your web controller
public class VideoController : ControllerBase
{
private readonly IMessageQueue _queue;
[HttpPost("upload")]
public async Task<IActionResult> Upload(IFormFile video)
{
await _storage.SaveAsync(video);
// Queue for processing - don't process in web app
await _queue.PublishAsync(new VideoTranscodeJob
{
VideoId = video.Id,
Priority = Priority.Normal
});
return Accepted(); // Return immediately
}
}
工人从队列中消耗 :
// In your worker service
public class VideoWorker : BackgroundService
{
private readonly IMessageQueue _queue;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await foreach (var job in _queue.SubscribeAsync<VideoTranscodeJob>(stoppingToken))
{
await TranscodeAsync(job);
}
}
}
流行信息队列选项 :
对于复杂的系统,责任划分如下:
/YourApp.Web # HTTP requests only
/YourApp.EmailWorker # Sends emails
/YourApp.VideoWorker # Transcodes videos
/YourApp.ReportWorker # Generates reports
/YourApp.Scheduler # Runs scheduled jobs (Hangfire)
每个工人可以:
尽管存在上述情况,但一些设想方案对于进程内背景服务来说是完全正常的:
// Fine to keep in web app
public class CacheWarmingService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await _cache.WarmupAsync(); // Quick operation
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
}
}
}
// Fine to keep in web app
public class FileWatcherService : IHostedService
{
// Reacts to events, doesn't consume significant resources
private FileSystemWatcher _watcher;
public Task StartAsync(CancellationToken cancellationToken)
{
_watcher = new FileSystemWatcher("/config");
_watcher.Changed += OnConfigChanged;
_watcher.EnableRaisingEvents = true;
return Task.CompletedTask;
}
}
// Fine to keep in web app if work is quick and not critical
public class EmailQueueService : BackgroundService
{
// Sends emails in background, but each email takes < 1 second
// If the app restarts, losing a few queued emails is acceptable
}
// Fine to keep in web app
public class WarmupService : IHostedService
{
// Runs once at startup, then does nothing
public async Task StartAsync(CancellationToken cancellationToken)
{
await _database.WarmupConnectionPoolAsync();
await _cache.LoadCriticalDataAsync();
}
}
在 Web App 中保留特性 |---------------|-----------------|------------------------| CPU 每次操作的使用量 < 100ms > 1 秒 * * < 100ms > * * * * * * * * * a second * * * * * * = * = * = * = * = * = * = * = * = * = * = * = * = * = * = * = = * = * = * = * = * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
在博客平台的代码 我们检查在第二部分:
在网络应用程序中保存 :
MarkdownDirectoryWatcherService - 轻量度文件监视器UmamiBackgroundSender - 快速分析事件EmailSenderHostedService - 体积小,非临界MarkdownReAddPostsService - 仅启动启动,配置配置如果规模扩大,应移到工人服务部门,如果:
BrokenLinkCheckerBackgroundService - 提出许多HTTP要求SemanticIndexingBackgroundService - 呼叫外部嵌入的API已在单独服务中:
Mostlylucid.SchedulerService - 点火仪表仪表板和通讯发送这是一个务实的方法: 开始简单( 正在处理中) , 当有需要的证据时分割 。
虽然 IHostedService 和 BackgroundService 有时你需要更精密的日程安排。那是图书馆喜欢的地方。 枪火 进来吧
枪火提供:
以下是一个简单的例子:
// In Program.cs
builder.Services.AddHangfire(config => config
.UsePostgreSqlStorage(connectionString)
.UseRecommendedSerializerSettings());
builder.Services.AddHangfireServer();
var app = builder.Build();
// Schedule recurring jobs
app.UseHangfireDashboard();
app.Services.GetRequiredService<IRecurringJobManager>()
.AddOrUpdate<NewsletterService>(
"send-daily-newsletter",
x => x.SendDailyNewsletter(),
Cron.Daily(17)); // 5 PM every day
您的服务只是普通的班级:
public class NewsletterService
{
private readonly IEmailService _emailService;
private readonly ISubscriberRepository _subscribers;
public NewsletterService(
IEmailService emailService,
ISubscriberRepository subscribers)
{
_emailService = emailService;
_subscribers = subscribers;
}
public async Task SendDailyNewsletter()
{
var subscribers = await _subscribers.GetDailySubscribersAsync();
foreach (var subscriber in subscribers)
{
await _emailService.SendNewsletterAsync(subscriber);
}
}
}
点火手柄:
graph TD
A[Hangfire Server] --> B{Check Schedule}
B -->|Job Due| C[Dequeue Job]
C --> D[Execute Job Method]
D -->|Success| E[Mark Complete]
D -->|Failure| F[Retry with Backoff]
F --> G{Max Retries?}
G -->|No| C
G -->|Yes| H[Mark Failed]
E --> I[Update Dashboard]
H --> I
I --> B
style A stroke:#059669,stroke-width:3px,color:#10b981
style D stroke:#2563eb,stroke-width:3px,color:#3b82f6
style E stroke:#059669,stroke-width:3px,color:#10b981
style H stroke:#dc2626,stroke-width:3px,color:#ef4444
何时使用点火 :
与 IHED Services/ BackServices / BackServices 粘贴时:
其它图书馆也值得考虑:
在第一部分中,我们在ASP.NET核心中涵盖了背景服务的基本方法:
最重要的教训是:
内 第二部分 第二部分我们将从一个制作博客平台上 审视现实世界的执行情况:
这些例子表明了第一部分行动的模式,包括启动协调模式和适当的关闭处理。
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.