From ABP Sensei
Guides through ABP Framework v10.4 infrastructure: distributed event bus, background jobs/workers, caching (Redis), BLOB storing, emailing, SignalR, distributed locking, entity cache, data filtering, seeding, settings, features, virtual file system, audit logging, and current user.
How this skill is triggered — by the user, by Claude, or both
Slash command
/abp-sensei:abp-infrastructureThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Guide to ABP Framework v10.4 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.
Guide to ABP Framework v10.4 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.
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> |
// Event class
public class StockCountChangedEvent
{
public Guid ProductId { get; set; }
public int NewCount { get; set; }
}
// Publish (from a service)
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
});
}
}
// Publish (from an Entity/Aggregate Root)
public class Product : AggregateRoot<Guid>
{
public void ChangeStockCount(int newCount)
{
StockCount = newCount;
AddLocalEvent(new StockCountChangedEvent { ProductId = Id, NewCount = newCount });
}
}
// Handler
public class StockChangeHandler : ILocalEventHandler<StockCountChangedEvent>, ITransientDependency
{
[UnitOfWork]
public virtual async Task HandleEventAsync(StockCountChangedEvent eventData)
{
// Event handling logic
}
}
Features:
LocalEventHandlerOrder// ETO (Event Transfer Object) — must be serializable
[EventName("MyApp.Product.StockChange")]
public class StockCountChangedEto
{
public Guid ProductId { get; set; }
public int NewCount { get; set; }
}
// Publish
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
});
}
}
// Publish from an entity
public class Product : AggregateRoot<Guid>
{
public void ChangeStockCount(int newCount)
{
StockCount = newCount;
AddDistributedEvent(new StockCountChangedEto { ProductId = Id, NewCount = newCount });
}
}
// Handler
public class StockChangeHandler : IDistributedEventHandler<StockCountChangedEto>, ITransientDependency
{
[UnitOfWork]
public virtual async Task HandleEventAsync(StockCountChangedEto eventData)
{
// Event handling logic
}
}
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 for data consistency with the distributed event bus:
Configure<AbpDistributedEventBusOptions>(options =>
{
options.InboxDatabaseName = "MyApp";
options.OutboxDatabaseName = "MyApp";
});
Background jobs are used to queue long-running operations and execute them in the background.
// Args class
public class EmailSendingArgs
{
public string EmailAddress { get; set; }
public string Subject { get; set; }
public string Body { get; set; }
}
// Job class
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);
}
}
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) // Optional delay
);
}
}
[BackgroundJobName("emails")]
public class EmailSendingArgs { }
| 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 |
Configure<AbpBackgroundJobOptions>(options =>
{
options.IsJobExecutionEnabled = false; // Don't run jobs, only enqueue them
});
// Cache Item
[CacheName("Books")]
public class BookCacheItem
{
public string Name { get; set; }
public float Price { get; set; }
}
// Usage
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)
}
);
}
}
Configure<AbpDistributedCacheOptions>(options =>
{
options.KeyPrefix = "MyApp1"; // Prefix on a shared cache server
options.GlobalCacheEntryOptions = new DistributedCacheEntryOptions
{
SlidingExpiration = TimeSpan.FromMinutes(20)
};
});
await _cache.GetManyAsync(keys);
await _cache.SetManyAsync(items);
await _cache.GetOrAddManyAsync(keys, async missingKeys => { ... });
await _cache.RemoveManyAsync(keys);
// Not written to the cache until the UOW succeeds
await _cache.SetAsync(key, value, considerUow: true);
abp add-package Volo.Abp.Caching.StackExchangeRedis
// appsettings.json
"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)Microsoft.Extensions.Caching.StackExchangeRedispublic 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.
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 |
[BlobContainerName("product-images")]
public class ProductImageBlobContainer : AbpBlobContainer { }
// Configuration
Configure<AbpBlobStoringOptions>(options =>
{
options.Containers.ConfigureDefault(configuring =>
{
configuring.UseFileSystem(fileSystem =>
{
fileSystem.BasePath = "C:\\blobs";
});
});
});
// Usage
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());
}
}
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) { }
}
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
);
}
}
await _emailSender.QueueAsync(
to: email,
subject: "Welcome!",
body: "Welcome...",
isBodyHtml: true
);
Emails sent via background job queue — tolerates errors with retry mechanism.
"Settings": {
"Abp.Mailing.Smtp.Host": "smtp.gmail.com",
"Abp.Mailing.Smtp.Port": "587",
"Abp.Mailing.Smtp.UserName": "[email protected]",
"Abp.Mailing.Smtp.Password": "password",
"Abp.Mailing.Smtp.EnableSsl": "true",
"Abp.Mailing.DefaultFromAddress": "[email protected]",
"Abp.Mailing.DefaultFromDisplayName": "My App"
}
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 => "[email protected]";
public string Password => "encrypted-password";
public bool EnableSsl => true;
public string DefaultFromAddress => "[email protected]";
public string DefaultFromDisplayName => "My App";
}
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);
}
}
public class MyEncryptionService : ISettingEncryptionService, ITransientDependency
{
public string Decrypt(string encryptedValue) { /* ... */ }
public string Encrypt(string plainValue) { /* ... */ }
}
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);
}
}
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;
}
// Disable the soft-delete filter
public async Task<List<Product>> GetAllIncludingDeletedAsync()
{
using (_dataFilter.Disable<ISoftDelete>())
{
return await _productRepository.GetListAsync();
}
}
// Disable the multi-tenancy filter
public async Task<long> GetAllTenantProductCountAsync()
{
using (_dataFilter.Disable<IMultiTenant>())
{
return await _productRepository.GetCountAsync();
}
}
}
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 definition
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
)
);
}
}
// Usage
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");
}
}
// Changing
await SettingManager.SetAsync("MyApp.MaxProductPrice", "2000");
// Feature definition
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"
)
);
}
}
// Usage
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");
}
}
// Add embedded files to the virtual file system
Configure<AbpVirtualFileSystemOptions>(options =>
{
options.FileSets.AddEmbedded<MyModule>();
});
// Usage (in a Razor view)
<link href="/MyModule/styles.css" rel="stylesheet" />
// Or with IVirtualFileProvider
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();
}
}
public class MyService : ApplicationService
{
// Pre-injected in ApplicationService/DomainService
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");
}
}
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)
{
// Lock acquired, critical operation
}
}
}
abp add-package Volo.Abp.AspNetCore.SignalR
[DependsOn(typeof(AbpAspNetCoreSignalRModule))]
public class MyModule : AbpModule { }
public class MessagingHub : Hub
{
public async Task SendMessage(string message)
{
await Clients.All.SendAsync("receiveMessage", message);
}
}
/signalr-hubs/messaging (kebab-case, without Hub suffix)[HubRoute("/my-messaging-hub")]public class MyHub : AbpHub
{
// Pre-injected: ICurrentTenant, ICurrentPrincipalAccessor, etc.
}
abp add-package @abp/signalr
abp install-libs
@using Volo.Abp.AspNetCore.Mvc.UI.Packages.SignalR
@section scripts {
<abp-script type="typeof(SignalRBrowserScriptContributor)" />
}
Configure<AbpSignalROptions>(options =>
{
options.Hubs.AddOrUpdate(
typeof(MessagingHub),
"/my-messaging/route",
hubOptions =>
{
hubOptions.LongPolling.PollTimeout = TimeSpan.FromSeconds(30);
}
);
});
public class MyService : ITransientDependency
{
private readonly IClock _clock;
public MyService(IClock clock) => _clock = clock;
public void DoSomething()
{
var now = _clock.Now; // UTC (default)
var localNow = _clock.Now.ToLocalTime();
}
}
// Timezone configuration
Configure<AbpClockOptions>(options =>
{
options.Kind = DateTimeKind.Utc; // or DateTimeKind.Local
});
GetOrAddAsync, set expiration durationsusing block; it is automatically restored outside the scopeProvides CDSS development patterns for drug interaction checking, dose validation, clinical scoring (NEWS2, qSOFA), and alert classification integrated into EMR workflows.
npx claudepluginhub burakdmir/abp-skills --plugin abp-sensei