| name | manage-units-of-work |
| description | Control ABP's Unit of Work — transaction and connection scope, explicit UOW scopes, the [UnitOfWork] attribute, and mid-scope SaveChangesAsync. USE FOR: starting explicit scopes with IUnitOfWorkManager.Begin, applying [UnitOfWork], choosing transactional vs non-transactional boundaries, calling SaveChangesAsync mid-scope, working with nested/RequiresNew/reserved scopes, reading IUnitOfWorkManager.Current, using OnCompleted/CompleteAsync/RollbackAsync. DO NOT USE FOR: defining a DbContext or repositories, or EF Core migrations (use ef-core-integration); MongoDB DbContext/repositories (use mongodb-integration); soft-delete/multi-tenant/custom query filters (use apply-data-filters); optimistic concurrency stamps (use handle-optimistic-concurrency); connection-string resolution (use configure-connection-strings).
|
| license | MIT |
Manage ABP Units of Work
ABP's Unit of Work (UOW) controls the database connection and transaction scope. Once a UOW starts it creates an ambient scope: every database operation done inside it participates in one boundary and is committed on success or rolled back on exception, all together.
When to Use
- Starting a UOW outside the conventions (background jobs, console apps, an inner scope) with
IUnitOfWorkManager.Begin.
- Enabling/disabling a UOW or its transaction on a method/class with
[UnitOfWork].
- Choosing transactional vs non-transactional boundaries, or changing the global default.
- Flushing pending changes mid-scope with
SaveChangesAsync (e.g. to read an auto-increment id).
- Running nested/
RequiresNew/reserved scopes, or hooking OnCompleted.
When Not to Use
- Defining a DbContext, repositories, or EF Core migrations — use the ef-core-integration skill.
- MongoDB DbContext / repositories — use mongodb-integration.
- Soft-delete / multi-tenant / custom query filters — use apply-data-filters.
- Optimistic concurrency stamps — use handle-optimistic-concurrency.
- Connection-string resolution/configuration — use configure-connection-strings.
How it works
When a UOW auto-completes (you usually don't touch it)
These method types are conventionally wrapped in a UOW and completed automatically when they return without throwing:
- ASP.NET Core MVC controller actions
- Razor page handlers
- Application service methods
- Repository methods
A UOW auto-begins for these unless there is already a surrounding (ambient) UOW — in that case the inner call silently participates in the outer one. So a repository called from an app service does not open its own UOW; it joins the app service's UOW. This is why you rarely start a UOW yourself.
Transactional or not (default behavior)
A UOW does not have to be transactional. By default:
- HTTP GET requests start a UOW but no database transaction.
- All other HTTP methods (POST/PUT/DELETE/PATCH/QUERY/...) start a UOW with a transaction, if the provider supports it — only GET is non-transactional by default. You can mark extra paths non-transactional via
AspNetCoreUnitOfWorkTransactionBehaviourProviderOptions.NonTransactionalUrls.
Change the global default via AbpUnitOfWorkDefaultOptions in your module's ConfigureServices:
Configure<AbpUnitOfWorkDefaultOptions>(options =>
{
options.TransactionBehavior = UnitOfWorkTransactionBehavior.Disabled;
});
AbpUnitOfWorkDefaultOptions also exposes Timeout (int?) and IsolationLevel (IsolationLevel?).
[UnitOfWork] attribute
Put [UnitOfWork] on a method or a class to enable/disable a UOW and control its transaction behavior. Interception rules: if you don't inject the service over an interface, the method must be virtual; only async methods (returning Task/Task<T>) are intercepted.
public class MyService : ITransientDependency
{
[UnitOfWork(IsTransactional = true)]
public virtual async Task FooAsync() { }
}
Attribute properties (from UnitOfWorkAttribute):
IsTransactional (bool?) — null means "decide by convention/config".
Timeout (int?) — milliseconds.
IsolationLevel (IsolationLevel?) — only meaningful when transactional.
IsDisabled (bool) — prevent starting a UOW for this method/class.
[UnitOfWork(IsDisabled = true)]
public virtual async Task NoUowAsync() { }
Important: if the method is called inside an ambient UOW, the attribute is ignored — the method just participates in the surrounding UOW. [UnitOfWork] only takes effect when it is the outermost scope.
Starting a UOW explicitly: IUnitOfWorkManager.Begin
Inject IUnitOfWorkManager when you need a UOW outside the conventions (background jobs, console apps, or an inner scope). The convenient extension overload:
public class MyService : ITransientDependency
{
private readonly IUnitOfWorkManager _unitOfWorkManager;
public MyService(IUnitOfWorkManager unitOfWorkManager)
{
_unitOfWorkManager = unitOfWorkManager;
}
public virtual async Task FooAsync()
{
using (var uow = _unitOfWorkManager.Begin(
requiresNew: true, isTransactional: true))
{
await uow.CompleteAsync();
}
}
}
Begin(requiresNew = false, isTransactional = false, isolationLevel = null, timeout = null):
requiresNew — false (default): if a surrounding UOW exists, Begin does not open a new one, it silently joins the existing UOW. true: ignore the ambient UOW and start a fresh, independent one (its own transaction).
- When you open a UOW manually via
Begin, you own its completion: call await uow.CompleteAsync() before leaving the using. If you don't complete it, disposing rolls it back.
There is also the lower-level Begin(AbpUnitOfWorkOptions options, bool requiresNew = false) on the interface itself.
The current UOW and SaveChangesAsync
UOW is ambient — reach it via IUnitOfWorkManager.Current (an IUnitOfWork), which is null when there is no surrounding UOW.
IUnitOfWork.SaveChangesAsync() flushes pending changes to the database mid-scope without ending the UOW. If the UOW is transactional, even those saved changes can still be rolled back on a later error. Typical use — get an auto-increment id right after insert:
public class CategoryAppService : ApplicationService, ICategoryAppService
{
private readonly IRepository<Category, int> _categoryRepository;
public CategoryAppService(IRepository<Category, int> categoryRepository)
=> _categoryRepository = categoryRepository;
public async Task<int> CreateAsync(string name)
{
var category = new Category { Name = name };
await _categoryRepository.InsertAsync(category);
await CurrentUnitOfWork.SaveChangesAsync();
return category.Id;
}
}
ApplicationService already exposes UnitOfWorkManager and the CurrentUnitOfWork shortcut, so you don't inject them there.
- Don't over-call it. All changes are saved automatically when the UOW ends without error. Only call
SaveChangesAsync() (or InsertAsync(..., autoSave: true)) when you truly need the flush now.
- With a
Guid primary key you never need to save just to get the id — it's set in-app immediately.
Nested and reserved UOW
- Nested / RequiresNew: call
Begin(requiresNew: true, ...) inside an existing scope to run an independent inner transaction that commits or rolls back separately from the outer one. Without requiresNew, the inner Begin just joins the outer UOW.
- Reserved UOW:
IUnitOfWorkManager.Reserve(reservationName, requiresNew) reserves a UOW slot up the stack; later BeginReserved(reservationName, options) / TryBeginReserved(...) starts the actual UOW under that reservation. IUnitOfWork.IsReserved and ReservationName reflect this. This is an advanced mechanism used internally (e.g. to make the outermost UOW transactional after inner code has already begun).
Useful IUnitOfWork members
CompleteAsync() / RollbackAsync() — commit or roll back a manually-started UOW.
OnCompleted(Func<Task>) — run a callback after the UOW successfully completes (changes are guaranteed saved).
Failed / Disposed events — react to failure or disposal.
Items — a Dictionary<string, object> to stash arbitrary state scoped to this UOW.
Options, Outer — the options it was started with and the enclosing UOW (if nested).
ASP.NET Core
The UOW system is fully integrated via action/page filters — normally zero config. If you need to widen the scope across other middleware, add app.UseUnitOfWork(); before app.UseConfiguredEndpoints();. UseUnitOfWork() is an IApplicationBuilder extension in the Microsoft.AspNetCore.Builder namespace, shipped by the Volo.Abp.AspNetCore package — a plain console/background-worker host that only references Volo.Abp.Uow won't have it.
Validation
- Inside a conventional method (controller action, page handler, app service, repository)
IUnitOfWorkManager.Current is non-null; outside any scope it is null.
- A manually-started
Begin scope must call await uow.CompleteAsync() to commit. Skipping it means an open transaction is never committed (and is disposed on Dispose()), so those changes are not persisted — but note this only covers work still buffered in an uncommitted transaction; non-transactional writes that already reached the database are not undone.
- Confirm transaction behavior: only GET requests run without a transaction by default; all other HTTP methods (including QUERY) run transactional (subject to
AbpUnitOfWorkDefaultOptions and any NonTransactionalUrls).
[UnitOfWork] takes effect only as the outermost scope — verify it is not being called inside an existing ambient UOW where it is ignored.
Common Pitfalls
[UnitOfWork] ignored inside an ambient UOW — the attribute only takes effect when it is the outermost scope; an inner call just joins the surrounding UOW.
- Forgetting
CompleteAsync() on a manual Begin — Dispose() disposes the transaction without committing it (it does not call RollbackAllAsync), so an uncommitted transactional UOW discards its changes — even writes already flushed with SaveChangesAsync are rolled back when the transaction is disposed (the real commit happens in CompleteAsync). What cannot be undone this way is writes made without a transaction, which hit the database as they happen.
Begin without requiresNew: true inside an existing scope silently joins the outer UOW instead of starting an independent transaction.
[UnitOfWork] not intercepted — the method must be async (Task/Task<T>), and virtual when the service is not injected over an interface.
- Over-calling
SaveChangesAsync() — changes flush automatically when the UOW ends; with a Guid PK you never need it just to read the id.
IUnitOfWorkManager.Current is null when no UOW surrounds the call — guard for it outside conventional scopes.