Search code examples
entity-frameworkaspnetboilerplate

AspNet Boilerplate Parallel DB Access through Entity Framework from an AppService


We are using ASP.NET Zero and are running into issues with parallel processing from an AppService. We know requests must be transactional, but unfortunately we need to break out to slow running APIs for numerous calls, so we have to do parallel processing.

As expected, we are running into a DbContext contingency issue on the second database call we make:

System.InvalidOperationException: A second operation started on this context 
before a previous operation completed. This is usually caused by different 
threads using the same instance of DbContext, however instance members are 
not guaranteed to be thread safe. This could also be caused by a nested query 
being evaluated on the client, if this is the case rewrite the query avoiding
nested invocations.

We read that a new UOW is required, so we tried using both the method attribute and the explicit UowManager, but neither of the two worked.

We also tried creating instances of the referenced AppServices using the IocResolver, but we are still not able to get a unique DbContext per thread (please see below).

public List<InvoiceDto> CreateInvoices(List<InvoiceTemplateLineItemDto> templateLineItems)
{
    List<InvoiceDto> invoices = new InvoiceDto[templateLineItems.Count].ToList();
    ConcurrentQueue<Exception> exceptions = new ConcurrentQueue<Exception>();

    Parallel.ForEach(templateLineItems, async (templateLineItem) =>
    {
        try
        {
            XAppService xAppService = _iocResolver.Resolve<XAppService>();
            InvoiceDto invoice = await xAppService
                .CreateInvoiceInvoiceItem();

            invoices.Insert(templateLineItems.IndexOf(templateLineItem), invoice);
        }
        catch (Exception e)
        {
            exceptions.Enqueue(e);
        }
    });

    if (exceptions.Count > 0) throw new AggregateException(exceptions);

    return invoices;
}

How can we ensure that a new DbContext is availble per thread?


Solution

  • I was able to replicate and resolve the problem with a generic version of ABP. I'm still experiencing the problem in my original solution, which is far more complex. I'll have to do some more digging to determine why it is failing there.

    For others that come across this problem, which is exactly the same issue as reference here, you can simply disable the UnitOfWork through an attribute as illustrated in the code below.

    public class InvoiceAppService : ApplicationService
    {
        private readonly InvoiceItemAppService _invoiceItemAppService;
    
        public InvoiceAppService(InvoiceItemAppService invoiceItemAppService)
        {
            _invoiceItemAppService = invoiceItemAppService;
        }
    
        // Just add this attribute
        [UnitOfWork(IsDisabled = true)]
        public InvoiceDto GetInvoice(List<int> invoiceItemIds)
        {
            _invoiceItemAppService.Initialize();
            ConcurrentQueue<InvoiceItemDto> invoiceItems = 
                new ConcurrentQueue<InvoiceItemDto>();
            ConcurrentQueue<Exception> exceptions = new ConcurrentQueue<Exception>();
            
            Parallel.ForEach(invoiceItemIds, (invoiceItemId) =>
            {
                try
                {
                    InvoiceItemDto invoiceItemDto = 
                        _invoiceItemAppService.CreateAsync(invoiceItemId).Result;
                    invoiceItems.Enqueue(invoiceItemDto);
                }
                catch (Exception e)
                {
                    exceptions.Enqueue(e);
                }
            });
    
            if (exceptions.Count > 0) {
                AggregateException ex = new AggregateException(exceptions);
                Logger.Error("Unable to get invoice", ex);
                throw ex;
            }
    
            return new InvoiceDto {
                Date = DateTime.Now,
                InvoiceItems = invoiceItems.ToArray()
            };
        }
    }
    
    public class InvoiceItemAppService : ApplicationService
    {
        private readonly IRepository<InvoiceItem> _invoiceItemRepository;
        private readonly IRepository<Token> _tokenRepository;
        private readonly IRepository<Credential> _credentialRepository;
        private Token _token;
        private Credential _credential;
    
        public InvoiceItemAppService(IRepository<InvoiceItem> invoiceItemRepository,
            IRepository<Token> tokenRepository,
            IRepository<Credential> credentialRepository)
        {
            _invoiceItemRepository = invoiceItemRepository;
            _tokenRepository = tokenRepository;
            _credentialRepository = credentialRepository;
        }
    
        public void Initialize()
        {
            _token = _tokenRepository.FirstOrDefault(x => x.Id == 1);
            _credential = _credentialRepository.FirstOrDefault(x => x.Id == 1);
        }
    
        // Create an invoice item using info from an external API and some db records
        public async Task<InvoiceItemDto> CreateAsync(int id)
        {
            // Get db record
            InvoiceItem invoiceItem = await _invoiceItemRepository.GetAsync(id);
    
            // Get price
            decimal price = await GetPriceAsync(invoiceItem.Description);
            
            return new InvoiceItemDto {
                Id = id,
                Description = invoiceItem.Description,
                Amount = price
            };
        }
    
        private async Task<decimal> GetPriceAsync(string description)
        {
            // Simulate a slow API call to get price using description
            // We use the token and credentials here in the real deal
            await Task.Delay(5000);
            return 100.00M;
        }
    }