Skip to main content

background-jobs-and-events

In-process/cross-service events, entity change reactions, deferred/recurring background work. USE FOR: ILocalEventBus / IDistributedEventBus, EntityCreated/Updated/DeletedEventData, [EventName] ETOs, IBackgroundJobManager jobs, AsyncPeriodicBackgroundWorkerBase workers. DO NOT USE FOR: distributed cache/lock (distributed-caching-and-locking); SignalR push (add-signalr-realtime); microservice message contracts (design-module-and-service-communication).

Zur Installation springen

Quellinformationen

Repository
abpframework/abp-skills
Letzte Quellaktivität
20. Juli 2026 um 03:24
Erkannte Sprache von SKILL.md
Englisch
Sterne
18
Forks
0

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
background-jobs-and-events
description
In-process/cross-service events, entity change reactions, deferred/recurring background work. USE FOR: ILocalEventBus / IDistributedEventBus, EntityCreated/Updated/DeletedEventData, [EventName] ETOs, IBackgroundJobManager jobs, AsyncPeriodicBackgroundWorkerBase workers. DO NOT USE FOR: distributed cache/lock (distributed-caching-and-locking); SignalR push (add-signalr-realtime); microservice message contracts (design-module-and-service-communication).
license
MIT
# Background Jobs & Events (ABP) Guidance for the ABP event bus (local + distributed), background jobs, and background workers. All APIs below are verified against ABP source. Inject services via constructor; the event bus only subscribes handlers that are already registered in DI, so a handler class must be registered (e.g. by implementing `ITransientDependency` or explicit registration) — implementing the handler interface alone is not enough. ## When to Use - Decouple code in the same process by publishing/handling local events. - React to entity create/update/delete performed through a repository. - Notify other microservices/apps of something that happened (distributed events). - Queue one-shot work to run later, with automatic retry on failure (background jobs). - Run recurring/periodic loops (cleanup, polling) as background workers. ## When Not to Use - **Coordinating a critical section across app instances** or **typed distributed caching** — use the **distributed-caching-and-locking** skill (`IAbpDistributedLock`, `IDistributedCache<T>`). - **Pushing real-time updates to connected clients** — use the **add-signalr-realtime** skill. - **Defining the message/contract shape between microservices** at the architecture level — use the **design-module-and-service-communication** skill. ## How it works ### Local event bus (in-process) `ILocalEventBus` (namespace `Volo.Abp.EventBus.Local`) publishes events within the same process. Publish via the inherited `IEventBus.PublishAsync`: ```csharp public class MyService : ITransientDependency { private readonly ILocalEventBus _localEventBus; public MyService(ILocalEventBus localEventBus) => _localEventBus = localEventBus; public async Task DoAsync() { // onUnitOfWorkComplete defaults to true: published at end of the current UoW await _localEventBus.PublishAsync(new StockCountChangedEvent { ProductId = 42, NewCount = 10 }); } } ``` Handle by implementing `ILocalEventHandler<TEvent>` (namespace `Volo.Abp.EventBus`). The handler is only subscribed if its class is registered in DI, so add `ITransientDependency` (or register it explicitly): ```csharp public class StockCountChangedHandler : ILocalEventHandler<StockCountChangedEvent>, ITransientDependency { public Task HandleEventAsync(StockCountChangedEvent eventData) { // ... return Task.CompletedTask; } } ``` A single class can implement multiple `ILocalEventHandler<T>` interfaces. ### Entity change events ABP automatically publishes local events when entities are created/updated/deleted through a repository. Handle them with the generic event data types from `Volo.Abp.Domain.Entities.Events`. The changed entity is on the `.Entity` property: ```csharp public class ProductCacheInvalidator : ILocalEventHandler<EntityCreatedEventData<Product>>, ILocalEventHandler<EntityUpdatedEventData<Product>>, ILocalEventHandler<EntityDeletedEventData<Product>>, ITransientDependency { public Task HandleEventAsync(EntityCreatedEventData<Product> e) => Invalidate(e.Entity); public Task HandleEventAsync(EntityUpdatedEventData<Product> e) => Invalidate(e.Entity); public Task HandleEventAsync(EntityDeletedEventData<Product> e) => Invalidate(e.Entity); private Task Invalidate(Product product) => Task.CompletedTask; } ``` Hierarchy: `EntityCreatedEventData<T>`, `EntityUpdatedEventData<T>`, `EntityDeletedEventData<T>` all derive from `EntityChangedEventData<T>`, which exposes `TEntity Entity { get; }`. Subscribe to `EntityChangedEventData<T>` to catch all three. ### Distributed event bus (cross-service) `IDistributedEventBus` (namespace `Volo.Abp.EventBus.Distributed`) publishes across processes/microservices through a provider (RabbitMQ, Kafka, Azure Service Bus, Dapr, Rebus, or the default in-memory one). Note the extra `useOutbox` parameter: ```csharp Task PublishAsync<TEvent>(TEvent eventData, bool onUnitOfWorkComplete = true, bool useOutbox = true) where TEvent : class; ``` Distributed events are transferred as serializable **ETOs** (Event Transfer Objects). Give the ETO a stable, transport-level name with `[EventName]` (namespace `Volo.Abp.EventBus`) so producer and consumer agree regardless of CLR type name: ```csharp [EventName("MyApp.Stock.Changed")] public class StockChangedEto { public Guid ProductId { get; set; } public int NewCount { get; set; } } // publisher await _distributedEventBus.PublishAsync(new StockChangedEto { ProductId = id, NewCount = count }); ``` Handle with `IDistributedEventHandler<TEto>` (namespace `Volo.Abp.EventBus.Distributed`): ```csharp public class StockChangedHandler : IDistributedEventHandler<StockChangedEto>, ITransientDependency { public Task HandleEventAsync(StockChangedEto eventData) => Task.CompletedTask; } ``` Provider selection is a module + configuration concern (e.g. `AbpEventBusRabbitMqModule`, `AbpEventBusKafkaModule`) plus settings in `appsettings.json`. Keep `useOutbox`/inbox enabled for reliable, transactional delivery when the provider is configured for it. ### Background jobs (deferred, one-shot work) Use `IBackgroundJobManager` (namespace `Volo.Abp.BackgroundJobs`) to queue work that runs later, retried on failure: ```csharp Task<string> EnqueueAsync<TArgs>( TArgs args, BackgroundJobPriority priority = BackgroundJobPriority.Normal, TimeSpan? delay = null); ``` ```csharp await _backgroundJobManager.EnqueueAsync( new EmailSendingArgs { To = "a@b.com", Subject = "Hi" }, delay: TimeSpan.FromSeconds(30)); ``` `BackgroundJobPriority` values: `Low`, `BelowNormal`, `Normal` (default), `AboveNormal`, `High`. Define the job by deriving from `AsyncBackgroundJob<TArgs>` (implements `IAsyncBackgroundJob<TArgs>`) and overriding `ExecuteAsync`. The args type is how the job is looked up. ```csharp public class EmailSendingJob : AsyncBackgroundJob<EmailSendingArgs>, ITransientDependency { public override async Task ExecuteAsync(EmailSendingArgs args) { // send email; a Logger property is available from the base class } } ``` For synchronous jobs, derive from `BackgroundJob<TArgs>` (`IBackgroundJob<TArgs>`) and override `void Execute(TArgs args)`. Prefer the async version. The startup template ships the Background Jobs module, which **persists jobs in the database by default** (so they survive restarts and are retried); the framework only falls back to an in-memory store when no persistent job-store module is installed. Hangfire, Quartz, and RabbitMQ are alternative providers you can swap in — not a requirement for production. ### Dynamic background jobs (enqueue by name) When the job type isn't known at compile time (plugin systems, dynamic workflows), use `IDynamicBackgroundJobManager` (namespace `Volo.Abp.BackgroundJobs`) to enqueue **by job name**: ```csharp // enqueue a typed job (named with [BackgroundJobName("emails")]) by its name await _dynamicJobManager.EnqueueAsync("emails", new { To = "user@example.com", Subject = "Hi" }); // or register a handler at runtime and enqueue against it _dynamicJobManager.RegisterHandler("ProcessOrder", async (context, ct) => { /* ... */ }); await _dynamicJobManager.EnqueueAsync("ProcessOrder", new { OrderId = orderId }); ``` `EnqueueAsync(name, args)` looks up the job by `[BackgroundJobName]` (or a runtime-registered handler), deserializes the args, and runs through the same persistent pipeline as typed jobs. Use typed `IBackgroundJobManager` when the job type is known; reach for the dynamic manager only when it genuinely isn't (the args are loosely typed, so you lose compile-time checking). ### Background workers (recurring work) For periodic loops, derive from `AsyncPeriodicBackgroundWorkerBase` (namespace `Volo.Abp.BackgroundWorkers`). Its constructor takes `AbpAsyncTimer timer` and `IServiceScopeFactory serviceScopeFactory`; set `timer.Period` (milliseconds) and override `DoWorkAsync`. Each run gets a fresh DI scope via `workerContext.ServiceProvider` — resolve scoped services from there, not the constructor: ```csharp public class CleanupWorker : AsyncPeriodicBackgroundWorkerBase { public CleanupWorker(AbpAsyncTimer timer, IServiceScopeFactory scopeFactory) : base(timer, scopeFactory) { Timer.Period = 60_000; // every 60s } protected override async Task DoWorkAsync(PeriodicBackgroundWorkerContext workerContext) { var repo = workerContext.ServiceProvider .GetRequiredService<IRepository<StaleRecord, Guid>>(); // ... do periodic work } } ``` `AsyncPeriodicBackgroundWorkerBase` also exposes a `CronExpression` property, but the **default in-memory worker only schedules by `Period`** — its `AbpAsyncTimer` ignores `CronExpression`. Cron scheduling is honored only when you use a Hangfire, Quartz, or TickerQ background-worker provider. Register the worker in your module's `OnApplicationInitializationAsync` with the extension `AddBackgroundWorkerAsync<TWorker>` on `ApplicationInitializationContext`: ```csharp public override async Task OnApplicationInitializationAsync(ApplicationInitializationContext context) { await context.AddBackgroundWorkerAsync<CleanupWorker>(); } ``` To add a worker at runtime, resolve `IBackgroundWorkerManager` and call `AddAsync(IBackgroundWorker worker)`. Custom workers can also derive from `BackgroundWorkerBase` (which implements `IBackgroundWorker`) for full control. ### Choosing the right tool - **Local event bus** — decouple code in the same process/app; entity change reactions. - **Distributed event bus** — notify other microservices/apps; use `[EventName]` + ETO. - **Background job** — run one-shot work later, with retries (emails, reports). - **Background worker** — run recurring/periodic work (cleanup, polling). ## Provider integrations (mainstream) Each provider needs its `[DependsOn]` module. The **configuration shape differs per provider** — some use `Configure<...Options>`, some `PreConfigure<...>`, and some also need an initialization-phase call (see the note below the examples). `appsettings.json` config works too, and values set in code take precedence over it. ### Background jobs **Hangfire** — depend on `AbpBackgroundJobsHangfireModule` and wire a Hangfire storage/server in `ConfigureServices` (Hangfire has no ABP options class; you configure Hangfire itself): ```csharp [DependsOn(typeof(AbpBackgroundJobsHangfireModule))] public class MyModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { var configuration = context.Services.GetConfiguration(); context.Services.AddHangfire(config => config.UseSqlServerStorage(configuration.GetConnectionString("Default"))); } } ``` **RabbitMQ** — depend on `AbpBackgroundJobsRabbitMqModule`; connections come from the `RabbitMQ:Connections` config (default = localhost), and `AbpRabbitMqBackgroundJobOptions` customizes queue names/connections per job: ```csharp Configure<AbpRabbitMqBackgroundJobOptions>(options => { options.DefaultQueueNamePrefix = "my_app_jobs."; options.JobQueues[typeof(EmailSendingArgs)] = new JobQueueConfiguration( typeof(EmailSendingArgs), queueName: "my_app_jobs.emails", delayedQueueName: "my_app_jobs.emails.delayed", // required ctor arg connectionName: "Default"); }); ``` ### Background workers Swap the default in-memory worker timer for a provider that honors `CronExpression` — but the two mainstream providers are **not** both zero-config: - **Hangfire** (`AbpBackgroundWorkersHangfireModule`) needs a **Hangfire storage/server configured**, exactly like the Hangfire *job* provider above (`AddHangfire(config => config.UseSqlServerStorage(...))`). The module adapts your workers but does **not** pick or configure storage — without it Hangfire fails to start. - **Quartz** (`AbpBackgroundWorkersQuartzModule`) runs on Quartz's default in-memory (RAM) store with no options; for production, configure a persistent Quartz store. ```csharp [DependsOn(typeof(AbpBackgroundWorkersQuartzModule))] // Quartz: in-memory store by default // or AbpBackgroundWorkersHangfireModule — then also configure Hangfire storage (see the Hangfire job example above) public class MyModule : AbpModule { } ``` Your existing `AsyncPeriodicBackgroundWorkerBase` workers, registered the same way with `context.AddBackgroundWorkerAsync<TWorker>()`, are then dispatched by the chosen provider (Hangfire adapts them via `HangfirePeriodicBackgroundWorkerAdapter`). This is what makes `CronExpression` honored on Quartz/Hangfire/TickerQ, unlike the default in-memory timer. ### Distributed event bus **RabbitMQ** — depend on `AbpEventBusRabbitMqModule`; `AbpRabbitMqEventBusOptions` sets `ConnectionName`, `ClientName` (the queue name for this app), and `ExchangeName`: ```csharp [DependsOn(typeof(AbpEventBusRabbitMqModule))] public class MyModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { Configure<AbpRabbitMqEventBusOptions>(options => { options.ClientName = "MyApp"; options.ExchangeName = "MyMessages"; }); } } ``` **Kafka** — depend on `AbpEventBusKafkaModule`; `AbpKafkaEventBusOptions` sets `ConnectionName`, `GroupId` (consumer group), and `TopicName`: ```csharp [DependsOn(typeof(AbpEventBusKafkaModule))] public class MyModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { Configure<AbpKafkaEventBusOptions>(options => { options.GroupId = "MyGroupId"; options.TopicName = "MyTopicName"; }); } } ``` The other providers (Quartz/TickerQ for jobs, Azure Service Bus/Rebus for the event bus) each add their `[DependsOn]` module, but the configuration shape is **not** uniform — check each provider's doc: - **Rebus** configures in `PreConfigureServices` via `PreConfigure<AbpRebusEventBusOptions>(...)`, not `Configure`. - **TickerQ** also needs an initialization-phase call — `context.GetHost().UseAbpTickerQ()` in `OnApplicationInitialization` (the `UseAbpTickerQ` extension is on `IHost`, not the context), not just options. - **Quartz** uses two options at different phases: `PreConfigure<AbpQuartzOptions>(...)` for the scheduler plus `Configure<AbpBackgroundJobQuartzOptions>(...)` for the job integration. ## Validation - Build the module. Workers are resolved and started at initialization; event handlers are resolved from DI each time the event fires, and jobs are resolved from DI at execution time. - For a local/distributed handler, confirm it is registered in DI (implements `ITransientDependency` or is registered explicitly) — an unregistered handler is silently never invoked. - For a background worker, confirm registration via `context.AddBackgroundWorkerAsync<TWorker>()` and observe `DoWorkAsync` firing on the configured `Timer.Period` (or the `CronExpression`, only if you run a Hangfire/Quartz/TickerQ provider). - For a background job, confirm `ExecuteAsync` runs after the configured `delay` (with a persistent provider wired in for anything beyond dev). ## Common Pitfalls - **Implementing the handler interface is not enough** — the event bus only subscribes handlers already registered in DI. Add `ITransientDependency` or register the class explicitly, or it never fires. - **Resolving scoped services from a background worker's constructor** — each `DoWorkAsync` run gets a fresh scope; resolve scoped services from `workerContext.ServiceProvider`, not the constructor. - **Relying on CLR type names for distributed events** — producer and consumer must agree on a stable `[EventName]` on the ETO, or delivery breaks when type names differ across services. - **Running background jobs without a persistent store** — the in-memory store is only the framework *fallback* used when neither the Background Jobs module nor a custom `IBackgroundJobStore` is installed. The startup template already installs the module with a database store (per your ORM), which **is** production-usable; Hangfire/Quartz/RabbitMQ are optional alternative providers, not a requirement. In a **cluster**, still configure a distributed lock so jobs execute on one instance. - **Assuming distributed delivery is reliable by default** — keep `useOutbox`/inbox enabled (with a provider configured for it) for transactional delivery.
Auf GitHub ansehen