| name | abp-infrastructure |
| description | ABP Framework v10.x (10.4/10.5) infrastructure: Distributed Event Bus, Background Jobs/Workers, Caching (Redis), BLOB Storing, Emailing, SignalR, IClock, Distributed Locking, Entity Cache. Use when you need an event bus, background job, cache, blob or email in ABP. |
ABP Framework — Infrastructure
Guide to ABP Framework v10.x (10.4/10.5) infrastructure components. Event Bus, Background Jobs, Caching, BLOB Storing, Emailing, Data Filtering, Data Seeding, Settings, Features, Virtual File System, Entity Cache, Distributed Locking, Audit Logging, Current User.
Trigger
- "ABP event bus"
- "ABP background job"
- "ABP cache"
- "ABP Redis"
- "ABP BLOB"
- "ABP email"
- "ABP data filter"
- "ABP data seeding"
- "ABP settings"
- "ABP features"
- "ABP virtual file"
- "ABP entity cache"
- "ABP distributed lock"
- "ABP audit log"
- "ABP current user"
- "ABP infrastructure"
Event Bus
ABP provides two types of event bus:
| Type | Usage | Interface |
|---|
| Local Event Bus | Within the same process | ILocalEventBus, ILocalEventHandler<TEvent> |
| Distributed Event Bus | Across processes (microservice) | IDistributedEventBus, IDistributedEventHandler<TEvent> |
Local Event Bus
public class StockCountChangedEvent
{
public Guid ProductId { get; set; }
public int NewCount { get; set; }
}
public class MyService : ITransientDependency
{
private readonly ILocalEventBus _localEventBus;
public MyService(ILocalEventBus localEventBus) => _localEventBus = localEventBus;
public async Task ChangeStockAsync(Guid productId, int newCount)
{
await _localEventBus.PublishAsync(new StockCountChangedEvent
{
ProductId = productId,
NewCount = newCount
});
}
}
public class Product : AggregateRoot<Guid>
{
public void ChangeStockCount(int newCount)
{
StockCount = newCount;
AddLocalEvent(new StockCountChangedEvent { ProductId = Id, NewCount = newCount });
}
}
public class StockChangeHandler : ILocalEventHandler<StockCountChangedEvent>, ITransientDependency
{
[UnitOfWork]
public virtual async Task HandleEventAsync(StockCountChangedEvent eventData)
{
}
}
Features:
- Event handlers run within the same UOW/transaction
- If a handler throws an exception, the transaction is rolled back
- Control execution order with
LocalEventHandlerOrder
- Events published from an entity are triggered on SaveChanges (EF Core)
Distributed Event Bus
[EventName("MyApp.Product.StockChange")]
public class StockCountChangedEto
{
public Guid ProductId { get; set; }
public int NewCount { get; set; }
}
public class MyService : ITransientDependency
{
private readonly IDistributedEventBus _distributedEventBus;
public MyService(IDistributedEventBus distributedEventBus) => _distributedEventBus = distributedEventBus;
public async Task ChangeStockAsync(Guid productId, int newCount)
{
await _distributedEventBus.PublishAsync(new StockCountChangedEto
{
ProductId = productId,
NewCount = newCount
});
}
}
public class Product : AggregateRoot<Guid>
{
public void ChangeStockCount(int newCount)
{
StockCount = newCount;
AddDistributedEvent(new StockCountChangedEto { ProductId = Id, NewCount = newCount });
}
}
public class StockChangeHandler : IDistributedEventHandler<StockCountChangedEto>, ITransientDependency
{
[UnitOfWork]
public virtual async Task HandleEventAsync(StockCountChangedEto eventData)
{
}
}
Distributed Event Bus Providers:
| Provider | Package | Description |
|---|
LocalDistributedEventBus | Default | In-process (for monolith) |
RabbitMqDistributedEventBus | Volo.Abp.EventBus.RabbitMQ | RabbitMQ |
KafkaDistributedEventBus | Volo.Abp.EventBus.Kafka | Apache Kafka |
AzureDistributedEventBus | Volo.Abp.EventBus.AzureServiceBus | Azure Service Bus |
RebusDistributedEventBus | Volo.Abp.EventBus.Rebus | Rebus |
Which Event Bus Should You Use?
| Scenario | Recommendation |
|---|
| Monolith (non-modular) | Local Event Bus |
| Modular Monolith | Distributed Event Bus (inter-module), Local (intra-module) |
| Microservice | Distributed Event Bus (inter-service), Local (intra-service) |
Inbox/Outbox Pattern (Distributed Event Bus)
Inbox/outbox pattern for data consistency with the distributed event bus:
Configure<AbpDistributedEventBusOptions>(options =>
{
options.InboxDatabaseName = "MyApp";
options.OutboxDatabaseName = "MyApp";
});
Background Jobs
Background jobs are used to queue long-running operations and execute them in the background.
Defining a Job
public class EmailSendingArgs
{
public string EmailAddress { get; set; }
public string Subject { get; set; }
public string Body { get; set; }
}
public class EmailSendingJob : AsyncBackgroundJob<EmailSendingArgs>, ITransientDependency
{
private readonly IEmailSender _emailSender;
public EmailSendingJob(IEmailSender emailSender) => _emailSender = emailSender;
public override async Task ExecuteAsync(EmailSendingArgs args)
{
await _emailSender.SendAsync(args.EmailAddress, args.Subject, args.Body);
}
}
Enqueuing a Job
public class RegistrationService : ApplicationService
{
private readonly IBackgroundJobManager _backgroundJobManager;
public RegistrationService(IBackgroundJobManager backgroundJobManager) => _backgroundJobManager = backgroundJobManager;
public async Task RegisterAsync(string email)
{
await _backgroundJobManager.EnqueueAsync(
new EmailSendingArgs { EmailAddress = email, Subject = "Welcome!", Body = "..." },
priority: BackgroundJobPriority.Normal,
delay: TimeSpan.FromMinutes(5)
);
}
}
Job Naming
[BackgroundJobName("emails")]
public class EmailSendingArgs { }
Background Job Providers
| Provider | Package | Description |
|---|
| Default | Volo.Abp.BackgroundJobs | In-memory, persistent (DB) |
| Hangfire | Volo.Abp.BackgroundJobs.Hangfire | Hangfire dashboard, retry |
| Quartz | Volo.Abp.BackgroundJobs.Quartz | Cron scheduling |
| RabbitMQ | Volo.Abp.BackgroundJobs.RabbitMQ | RabbitMQ queue |
Disabling Job Execution
Configure<AbpBackgroundJobOptions>(options =>
{
options.IsJobExecutionEnabled = false;
});
Caching
IDistributedCache
[CacheName("Books")]
public class BookCacheItem
{
public string Name { get; set; }
public float Price { get; set; }
}
public class BookService : ITransientDependency
{
private readonly IDistributedCache<BookCacheItem, Guid> _cache;
public BookService(IDistributedCache<BookCacheItem, Guid> cache) => _cache = cache;
public async Task<BookCacheItem> GetAsync(Guid bookId)
{
return await _cache.GetOrAddAsync(
bookId,
async () => await GetBookFromDatabaseAsync(bookId),
() => new DistributedCacheEntryOptions
{
AbsoluteExpiration = DateTimeOffset.Now.AddHours(1)
}
);
}
}
Configuration
Configure<AbpDistributedCacheOptions>(options =>
{
options.KeyPrefix = "MyApp1";
options.GlobalCacheEntryOptions = new DistributedCacheEntryOptions
{
SlidingExpiration = TimeSpan.FromMinutes(20)
};
});
Batch Operations
await _cache.GetManyAsync(keys);
await _cache.SetManyAsync(items);
await _cache.GetOrAddManyAsync(keys, async missingKeys => { ... });
await _cache.RemoveManyAsync(keys);
UOW-Level Cache
await _cache.SetAsync(key, value, considerUow: true);
Redis Cache
abp add-package Volo.Abp.Caching.StackExchangeRedis
"Redis": {
"IsEnabled": "true",
"Configuration": "127.0.0.1"
}
Or via code:
Configure<RedisCacheOptions>(options => { });
Why use Volo.Abp.Caching.StackExchangeRedis?
SetManyAsync and GetManyAsync implementations (the Microsoft package lacks them)
- Simplified Redis configuration
- Built on top of
Microsoft.Extensions.Caching.StackExchangeRedis
Entity Cache
public class BookEntityCache : EntityCache<Book, BookCacheItem>, ITransientDependency
{
public BookEntityCache(ICache<Book> cache) : base(cache) { }
}
Entity cache is read-only and is automatically invalidated on entity update/delete.
BLOB Storing
An abstraction for file storage. Various providers are supported:
| Provider | Package |
|---|
| File System | Volo.Abp.BlobStoring.FileSystem |
| Database | Volo.Abp.BlobStoring.Database |
| AWS S3 | Volo.Abp.BlobStoring.Aws |
| Azure | Volo.Abp.BlobStoring.Azure |
| MinIO | Volo.Abp.BlobStoring.Minio |
| Google Cloud | Volo.Abp.BlobStoring.Google |
| Alibaba Cloud | Volo.Abp.BlobStoring.Aliyun |
| Bunny CDN | Volo.Abp.BlobStoring.Bunny |
| Memory | Volo.Abp.BlobStoring.Memory |
Usage
[BlobContainerName("product-images")]
public class ProductImageBlobContainer : AbpBlobContainer { }
Configure<AbpBlobStoringOptions>(options =>
{
options.Containers.ConfigureDefault(configuring =>
{
configuring.UseFileSystem(fileSystem =>
{
fileSystem.BasePath = "C:\\blobs";
});
});
});
public class ProductImageService : ITransientDependency
{
private readonly IBlobContainer<ProductImageBlobContainer> _blobContainer;
public ProductImageService(IBlobContainer<ProductImageBlobContainer> blobContainer) =>
_blobContainer = blobContainer;
public async Task SaveAsync(Guid productId, byte[] imageBytes)
{
await _blobContainer.SaveAsync(productId.ToString(), imageBytes, true);
}
public async Task<byte[]> GetAsync(Guid productId)
{
return await _blobContainer.GetAllAsync(productId.ToString());
}
}
Custom Provider
public class MyBlobProvider : IBlobProvider, ITransientDependency
{
public async Task SaveAsync(BlobProviderArgs args) { }
public async Task<byte[]> GetAsync(BlobProviderArgs args) { }
public async Task<bool> ExistsAsync(BlobProviderArgs args) { }
public async Task DeleteAsync(BlobProviderArgs args) { }
}
Emailing (MailKit)
abp add-package Volo.Abp.MailKit
public class MyService : ITransientDependency
{
private readonly IEmailSender _emailSender;
public MyService(IEmailSender emailSender) => _emailSender = emailSender;
public async Task SendWelcomeEmailAsync(string email)
{
await _emailSender.SendAsync(
to: email,
subject: "Welcome!",
body: "Welcome to our platform...",
isBodyHtml: true
);
}
}
Queueing Emails (Background Jobs)
await _emailSender.QueueAsync(
to: email,
subject: "Welcome!",
body: "Welcome...",
isBodyHtml: true
);
Emails sent via background job queue — tolerates errors with retry mechanism.
Configuration
"Settings": {
"Abp.Mailing.Smtp.Host": "smtp.gmail.com",
"Abp.Mailing.Smtp.Port": "587",
"Abp.Mailing.Smtp.UserName": "user@gmail.com",
"Abp.Mailing.Smtp.Password": "password",
"Abp.Mailing.Smtp.EnableSsl": "true",
"Abp.Mailing.DefaultFromAddress": "noreply@myapp.com",
"Abp.Mailing.DefaultFromDisplayName": "My App"
}
ISmtpEmailSenderConfiguration
Custom configuration source (instead of settings system):
public class MySmtpConfiguration : ISmtpEmailSenderConfiguration, ITransientDependency
{
public string Host => "smtp.mycompany.com";
public int Port => 587;
public string UserName => "app@mycompany.com";
public string Password => "encrypted-password";
public bool EnableSsl => true;
public string DefaultFromAddress => "noreply@mycompany.com";
public string DefaultFromDisplayName => "My App";
}
Text Template Integration
public class MyService : ITransientDependency
{
private readonly IEmailSender _emailSender;
private readonly ITextTemplateRenderer _textTemplateRenderer;
public async Task SendTemplateEmailAsync(string email)
{
var body = await _textTemplateRenderer.RenderAsync(
"WelcomeEmailTemplate",
new Dictionary<string, object>
{
{ "userName", "John" },
{ "activationLink", "https://myapp.com/activate/123" }
}
);
await _emailSender.SendAsync(email, "Welcome!", body, isBodyHtml: true);
}
}
Encrypt SMTP Password
public class MyEncryptionService : ISettingEncryptionService, ITransientDependency
{
public string Decrypt(string encryptedValue) { }
public string Encrypt(string plainValue) { }
}
SMS (Twilio)
abp add-package Volo.Abp.Sms.Twilio
public class MyService : ITransientDependency
{
private readonly ITwilioSmsSender _smsSender;
public MyService(ITwilioSmsSender smsSender) => _smsSender = smsSender;
public async Task SendSmsAsync(string phone, string message)
{
await _smsSender.SendAsync(phone, message);
}
}
Data Filtering
public class MyService : ITransientDependency
{
private readonly IDataFilter _dataFilter;
private readonly IRepository<Product, Guid> _productRepository;
public MyService(IDataFilter dataFilter, IRepository<Product, Guid> productRepository)
{
_dataFilter = dataFilter;
_productRepository = productRepository;
}
public async Task<List<Product>> GetAllIncludingDeletedAsync()
{
using (_dataFilter.Disable<ISoftDelete>())
{
return await _productRepository.GetListAsync();
}
}
public async Task<long> GetAllTenantProductCountAsync()
{
using (_dataFilter.Disable<IMultiTenant>())
{
return await _productRepository.GetCountAsync();
}
}
}
Data Seeding
public class MyDataSeedContributor : IDataSeedContributor, ITransientDependency
{
private readonly IRepository<IdentityRole, Guid> _roleRepository;
public MyDataSeedContributor(IRepository<IdentityRole, Guid> roleRepository) =>
_roleRepository = roleRepository;
public async Task SeedAsync(DataSeedContext context)
{
if (await _roleRepository.FindAsync(x => x.Name == "Admin") == null)
{
await _roleRepository.InsertAsync(new IdentityRole(GuidGenerator.Create(), "Admin"));
}
}
}
Settings
public class MyAppSettings : SettingDefinitionProvider
{
public override void Define(ISettingDefinitionContext context)
{
context.Add(
new SettingDefinition(
name: "MyApp.MaxProductPrice",
defaultValue: "1000",
displayName: "Maximum Product Price",
description: "Maximum allowed product price",
isVisibleToClients: true
)
);
}
}
public class MyService : ITransientDependency
{
private readonly ISettingProvider _settingProvider;
public MyService(ISettingProvider settingProvider) => _settingProvider = settingProvider;
public async Task<decimal> GetMaxPriceAsync()
{
return await _settingProvider.GetOrNullAsync<decimal>("MyApp.MaxProductPrice");
}
}
await SettingManager.SetAsync("MyApp.MaxProductPrice", "2000");
Features
public class MyAppFeatures : FeatureDefinitionProvider
{
public override void Define(IFeatureDefinitionContext context)
{
var myFeature = context.Add(
new FeatureDefinition(
name: "MyApp.PremiumFeature",
defaultValue: "false",
displayName: "Premium Feature",
description: "Enables premium features"
)
);
}
}
public class MyService : ITransientDependency
{
private readonly IFeatureChecker _featureChecker;
public MyService(IFeatureChecker featureChecker) => _featureChecker = featureChecker;
public async Task<bool> IsPremiumEnabledAsync()
{
return await _featureChecker.IsEnabledAsync("MyApp.PremiumFeature");
}
}
Virtual File System
Configure<AbpVirtualFileSystemOptions>(options =>
{
options.FileSets.AddEmbedded<MyModule>();
});
<link href="/MyModule/styles.css" rel="stylesheet" />
public class MyService : ITransientDependency
{
private readonly IVirtualFileProvider _virtualFileProvider;
public MyService(IVirtualFileProvider virtualFileProvider) => _virtualFileProvider = virtualFileProvider;
public async Task<string> GetFileContentAsync(string path)
{
var fileInfo = _virtualFileProvider.GetFileInfo(path);
using var reader = new StreamReader(fileInfo.CreateReadStream());
return await reader.ReadToEndAsync();
}
}
Current User
public class MyService : ApplicationService
{
public async Task DoSomethingAsync()
{
var userId = CurrentUser.Id;
var userName = CurrentUser.UserName;
var tenantId = CurrentUser.TenantId;
var roles = CurrentUser.Roles;
var email = CurrentUser.FindClaimValue("email");
var isAuthenticated = CurrentUser.IsAuthenticated;
var isAdmin = CurrentUser.IsInRole("admin");
}
}
Distributed Locking
public class MyService : ITransientDependency
{
private readonly IDistributedLockProvider _distributedLock;
public MyService(IDistributedLockProvider distributedLock) => _distributedLock = distributedLock;
public async Task DoSomethingAsync()
{
await using var handle = await _distributedLock.TryAcquireAsync("my-lock-key");
if (handle != null)
{
}
}
}
SignalR Integration
abp add-package Volo.Abp.AspNetCore.SignalR
Server Side
[DependsOn(typeof(AbpAspNetCoreSignalRModule))]
public class MyModule : AbpModule { }
Hub Definition
public class MessagingHub : Hub
{
public async Task SendMessage(string message)
{
await Clients.All.SendAsync("receiveMessage", message);
}
}
- ABP auto-registers hubs to DI (transient)
- Auto-maps hub endpoint at
/signalr-hubs/messaging (kebab-case, without Hub suffix)
- Custom route:
[HubRoute("/my-messaging-hub")]
AbpHub Base Classes
public class MyHub : AbpHub
{
}
Client Side (MVC/Razor Pages)
abp add-package @abp/signalr
abp install-libs
@using Volo.Abp.AspNetCore.Mvc.UI.Packages.SignalR
@section scripts {
<abp-script type="typeof(SignalRBrowserScriptContributor)" />
}
SignalR Options
Configure<AbpSignalROptions>(options =>
{
options.Hubs.AddOrUpdate(
typeof(MessagingHub),
"/my-messaging/route",
hubOptions =>
{
hubOptions.LongPolling.PollTimeout = TimeSpan.FromSeconds(30);
}
);
});
Timing & Timezone
public class MyService : ITransientDependency
{
private readonly IClock _clock;
public MyService(IClock clock) => _clock = clock;
public void DoSomething()
{
var now = _clock.Now;
var localNow = _clock.Now.ToLocalTime();
}
}
Configure<AbpClockOptions>(options =>
{
options.Kind = DateTimeKind.Utc;
});
Best Practices
- Event Bus: Publish events from the entity, handle them in the service
- Background Jobs: Move long-running operations to a job, don't keep the user waiting
- Caching: Use
GetOrAddAsync, set expiration durations
- BLOB Storing: Use the provider abstraction, don't depend directly on the storage API
- Data Filtering: Disable inside a
using block; it is automatically restored outside the scope
- Settings: Use for runtime-changeable settings
- Features: Use for per-tenant feature toggles
- Distributed Lock: Use for operations requiring concurrent execution control
What's New in v10.5
S3-Compatible Blob Storage (v10.5+)
The AWS blob provider (Volo.Abp.BlobStoring.Aws) now supports S3-compatible services (Cloudflare R2, MinIO, Backblaze B2, Wasabi, DigitalOcean Spaces) via a custom endpoint:
Configure<AbpBlobStoringOptions>(options =>
{
options.Containers.ConfigureDefault(container =>
{
container.UseAws(aws =>
{
aws.ServiceURL = "https://<account-id>.r2.cloudflarestorage.com";
aws.DisablePayloadSigning = true;
});
});
});
Keep DisablePayloadSigning off for providers that support default AWS SDK signing (including real S3).
Dynamic Background Worker Capability Markers (v10.5+)
Dynamic background worker managers expose their capabilities via marker interfaces:
ISupportsRuntimeRegistration — worker can be registered at runtime.
ISupportsCronScheduling — worker supports cron expressions.
Hangfire and Quartz managers implement both. The default in-memory manager supports runtime registration only and rejects cron expressions; TickerQ's dynamic manager exposes neither. If you build UI or integration logic on IDynamicBackgroundWorkerManager, check these markers before offering runtime registration or cron scheduling; use Hangfire or Quartz when runtime cron scheduling is required.
Related