- Template: app
- Template Type: Classic
- Created ABP Studio Version: 2.2.1
- Current ABP Studio Version: 3.0.6
- Tiered: No
- Multi-Tenancy: Yes
- UI Framework: angular
- Theme: leptonx
- Theme Style: system
- Theme Menu Placement: side
- Progressive Web App: No
- Run Progressive Web App Support: No
- Database Provider: ef
- Database Management System: sqlserver
- Separate Tenant Schema: Yes
- Mobile Framework: none
- Public Website: No
- Social Login: Yes
- Include Tests: Yes
- Kubernetes Configuration: Yes
- Distributed Event Bus: none
- Use Local References: No
- Optional Modules:
- GDPR
- FileManagement
- TextTemplateManagement
- LanguageManagement
- AuditLogging
- OpenIddictAdmin
- Selected Languages: German (Switzerland), English, French, German (Germany), Italian
- Default Language: German (Switzerland)
- Create Command: abp new Bpo -t app --ui-framework angular --database-provider ef --database-management-system sqlserver --theme leptonx --separate-tenant-schema --skip-migration --skip-migrator --without-cms-kit --dont-run-install-libs --dont-run-bundling -file-management
In our application, we have a service that writes a very large amount of data to the database. This generates more than 1 million inserts into the database.
I’ve now optimized this service so that it completes in about 100 seconds. I disabled transactions for the UnitOfWork to avoid additional overhead (isTransactional: false). The data is now stored in the database after just under 2 minutes.
Problem: Completing the UoW (uow.CompleteAsync()) takes about 17 hours !!!
Since I don’t see any log messages, I have no idea what’s taking so long. My guess is that events are being generated (which are completely unnecessary in my case).
The affected entities are not auditable. I also added a [DisableAuditing] attribute to the AppService methods.
I’ve now moved the service call to a background job so that it doesn’t block anything. But it’s still very unsatisfying this way.
What can be done to optimize this?
protected override async Task ExecuteWithContextAsync(InstallationJobArgs args)
{
await _jobStatusStore.SetRunningAsync(args.JobId);
try
{
var data = await LoadDataAsync(args);
using var uow = _unitOfWorkManager.Begin(requiresNew: true, isTransactional: false);
await _installer.InstallAsync(data); // Takes 100 seconds
await uow.CompleteAsync(); // Takes 17 hours
await _jobStatusStore.SetSucceededAsync(args.JobId);
}
catch (Exception ex)
{
await _jobStatusStore.SetFailedAsync(args.JobId, ex.Message);
throw;
}
}
6 Answer(s)
-
0
uow.CompleteAsync()in ABP does more than “commit”. Even whenisTransactional: false, it still runs the Unit Of Work completion pipeline, which includes (among other things) publishing/processing “auto events” (domain/entity change events) inside the same UoW. With ~1,000,000 inserts, any “per-entity” event collection/publication/handler execution can dominate the runtime and look like “hangs for hours”.The fix is to avoid accumulating 1M changes in a single UoW and avoid/disable auto-event generation/handling for bulk imports.
What to do (practical, ABP-friendly)
- Split the import into batches and complete multiple UoWs
- E.g. process 5k–50k entities, then
CompleteAsync(), then start a new UoW. - This prevents ABP from holding a gigantic “changed entity / event” list until the very end.
- E.g. process 5k–50k entities, then
const int batchSize = 10000; foreach (var batch in data.Chunk(batchSize)) { using var uow = _unitOfWorkManager.Begin(requiresNew: true, isTransactional: false); await _installer.InstallBatchAsync(batch); await uow.CompleteAsync(); }Disable/avoid auto-event publishing for this import path
- Your suspicion is very likely correct: ABP’s “auto events” are handled in the same unit of work (so they run during
CompleteAsync()). - If you have any event handlers (local/distributed) hooked to entity changes (directly or indirectly), they can be triggered massively.
The exact way to disable auto-event publishing depends on what you’re using (domain events, entity change events, distributed events, etc.). However, the most robust approach for “1M inserts import” is:
- Do not use regular repository insert/update APIs that trigger the ABP entity change/event pipeline, or
- Use a bulk import approach (EFCore.BulkExtensions / SqlBulkCopy) in a separate, dedicated path (so ABP’s UoW/event mechanisms are bypassed).
- Your suspicion is very likely correct: ABP’s “auto events” are handled in the same unit of work (so they run during
If you keep EF Core inserts: flush periodically
- Even within one UoW, forcing periodic
SaveChangesAsync()can reduce memory/event buildup, but it still leaves the finalCompleteAsync()pipeline cost. Batching UoWs is usually better.
- Even within one UoW, forcing periodic
Why
[DisableAuditing]didn’t help- Auditing is only one part of the pipeline.
CompleteAsync()can still be slow due to auto-event processing and other UoW completion tasks, even if auditing is disabled and the entities aren’t auditable.
Sources
- https://abp.io/docs/latest/framework/architecture/domain-driven-design/unit-of-work
- https://abp.io/docs/latest/release-info/migration-guides/abp-5-0
- https://abp.io/community/articles/0r248xsr
This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) -
0
Hi,
Could you share
_installer.InstallAsync? We'd like to see exactly how the rows are being written —IRepository<T>.InsertAsyncin a loop,InsertManyAsync,_dbContext.AddRange + SaveChangesAsync, or something else.One thing to flag up front —
[DisableEntityChangeTracking]andrepository.DisableTracking()only affect read queries, notInsertAsync/UpdateAsync. The docs are explicit about this:Change tracking behavior doesn't affect tracking entity objects returned from
InsertAsyncandUpdateAsyncmethods.So those attributes won't change anything on the insert path.
Here's an approach you can try — a small replacement for
IEntityChangeEventHelperthat suppresses entity events for an explicitusingscope. Could you drop this into your project, wrap the import call site like below, and let us know whetherCompleteAsyncdrops to something reasonable on your 1M case?[Dependency(ReplaceServices = true)] [ExposeServices(typeof(IEntityChangeEventHelper), typeof(EntityChangeEventHelper))] public class ScopedEntityChangeEventHelper : EntityChangeEventHelper { private static readonly AsyncLocal<bool> _suppressed = new(); public ScopedEntityChangeEventHelper( IUnitOfWorkManager uowManager, IEntityToEtoMapper etoMapper, IOptions<AbpDistributedEntityEventOptions> options) : base(uowManager, etoMapper, options) { } public static IDisposable Suppress() { var prev = _suppressed.Value; _suppressed.Value = true; return new DisposeAction(() => _suppressed.Value = prev); } public override void PublishEntityCreatedEvent(object entity) { if (!_suppressed.Value) base.PublishEntityCreatedEvent(entity); } public override void PublishEntityUpdatedEvent(object entity) { if (!_suppressed.Value) base.PublishEntityUpdatedEvent(entity); } public override void PublishEntityDeletedEvent(object entity) { if (!_suppressed.Value) base.PublishEntityDeletedEvent(entity); } }Usage at your import call site —
_installer.InstallAsyncitself stays unchanged:using var _ = ScopedEntityChangeEventHelper.Suppress(); using var uow = _unitOfWorkManager.Begin(requiresNew: true, isTransactional: false); await _installer.InstallAsync(data); await uow.CompleteAsync();The effect is scoped to the
usingblock, so the rest of your app keeps behaving normally.To be clear about what this does and doesn't touch — it only intercepts the per-entity broadcast events (
EntityCreatedEventData<T>/EntityUpdatedEventData<T>/EntityDeletedEventData<T>and their distributedEtocounterparts). Everything else ABP normally does for tracked entities keeps running:CreationTime/CreatorId/LastModificationTime/ConcurrencyStampare still set automatically, multi-tenancy and soft-delete still work, and any domain events you raise yourself withentity.AddLocalEvent(...)/AddDistributedEvent(...)go through a different pipeline and are not affected.If you do have any local or distributed event handlers subscribed to
EntityCreatedEventData<T>/EntityCreatedEto<T>for the entities being inserted, those will be skipped during the import — usually fine for a bulk install, but worth confirming on your side.Thanks
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) -
0
Hi, thanks for this detailed answer. This is exactly the kind of solution I was looking for. I’ll be happy to test it out, and I think this is what I need.
_installer.InstallAsync()is relatively complex (recursive traversal and inserts across multiple repositories), so I can’t share it here. But if your approach doesn’t work, I’ll send you the code via email.By the way: after 1000 inserts, I call
await _unitOfWorkManager.Current!.SaveChangesAsync(). I also clear the DbContext’s ChangeTracker. The data is also fully present in the database. That’s why I suspect the problem might be in postprocessing (event pipeline).Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) -
0
Hi,
Sounds good. Once you've had a chance to try it, let us know how it goes. If anything doesn't work as expected, share whatever details you can — timings, a small repro, or send the code via email if that's easier — and we'll keep digging on our side.
Thanks
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)