ASP.NET Zero中AppService并行处理时如何确保线程使用独立DbContext?
Great question! Let's break down what's going wrong here and how to fix it properly for your scenario.
The Core Issues With Your Current Approach
Parallel.ForEach+async/awaitis a bad matchParallel.ForEachis built for synchronous CPU-bound work. When you pass anasynclambda to it, it treats the lambda asasync void—meaning it won't wait for your asynchronous API calls to complete. This leads to race conditions, premature resource disposal, and the DbContext conflicts you're hitting.No isolated dependency injection scopes
ASP.NET Zero (and the Abp framework) uses scoped services for DbContexts by default, tied to the request scope. When you resolveXAppServicedirectly in parallel threads without creating a dedicated scope, all threads end up sharing the same scoped DbContext instance—which isn't thread-safe.
Step-by-Step Solution
Here's how to adjust your code to ensure each parallel task gets its own DbContext and maintains transactionality:
1. Switch to Async-Friendly Parallelism with Task.WhenAll
Replace Parallel.ForEach with Task.WhenAll, which is designed to handle asynchronous tasks properly and wait for all of them to finish.
2. Create Isolated DI Scopes for Each Task
Use IIocManager.CreateScope() to create a separate dependency injection scope for each task. This guarantees every task gets its own instance of XAppService and associated DbContext.
3. Explicitly Manage UOW (If Needed)
If your CreateInvoiceInvoiceItem method doesn't already have the [UnitOfWork] attribute, manually start and complete a UOW within each scope to keep database operations transactional.
Updated Code Example
First, inject IIocManager and IUowManager into your parent AppService:
private readonly IIocManager _iocManager; private readonly IUowManager _uowManager; public YourParentAppService(IIocManager iocManager, IUowManager uowManager) { _iocManager = iocManager; _uowManager = uowManager; }
Then rewrite your CreateInvoices method:
public async Task<List<InvoiceDto>> CreateInvoices(List<InvoiceTemplateLineItemDto> templateLineItems) { // Optional: Limit concurrent API calls to avoid throttling the external service var concurrencySemaphore = new SemaphoreSlim(5); var taskResults = new List<(InvoiceDto Invoice, Exception Error)>(); // Create async tasks for each template line item var tasks = templateLineItems.Select(async templateLineItem => { await concurrencySemaphore.WaitAsync(); try { using (var scope = _iocManager.CreateScope()) // Isolate DI scope for this task { var xAppService = scope.Resolve<XAppService>(); using (var uow = _uowManager.Begin()) // Start independent UOW { var invoice = await xAppService.CreateInvoiceInvoiceItem(); await uow.CompleteAsync(); // Commit the transaction return (Invoice: invoice, Error: null); } } } catch (Exception ex) { return (Invoice: null, Error: ex); } finally { concurrencySemaphore.Release(); } }); // Wait for all tasks to complete taskResults = (await Task.WhenAll(tasks)).ToList(); // Collect and rethrow any exceptions var exceptions = taskResults.Where(r => r.Error != null).Select(r => r.Error).ToList(); if (exceptions.Any()) { throw new AggregateException("One or more invoice creation tasks failed", exceptions); } // Return results in the original order (Task.WhenAll preserves input ordering) return taskResults.Select(r => r.Invoice).ToList(); }
Key Details to Note
- DI Scopes: The
using (var scope = _iocManager.CreateScope())block ensures every task gets its own scoped services (including DbContext), eliminating concurrency conflicts entirely. - UOW Management: The manual UOW block guarantees each task's database operations run in a separate transaction, maintaining the transactionality you require.
- Concurrency Control: The
SemaphoreSlimlimits how many API calls run at once, preventing you from overwhelming the external API with too many parallel requests. - Error Handling: We capture exceptions per task and rethrow them as an
AggregateException, so you can see all failures at once instead of just the first one.
Additional Checks
- Ensure your
XAppServiceis registered as a Scoped or Transient service (this is the default in ASP.NET Zero, but double-check if you've modified service registrations). - If
CreateInvoiceInvoiceItemalready has the[UnitOfWork]attribute, you can remove the manual UOW block—Abp will automatically handle the transaction within the scope.
内容的提问来源于stack exchange,提问作者André Haupt

