通过 Entity Framework 来自 AppService 的 AspNet 样板并行数据库访问

AspNet Boilerplate Parallel DB Access through Entity Framework from an AppService

我们正在使用 ASP.NET 零并且 运行 遇到来自 AppService 的并行处理问题。我们知道请求必须是事务性的,但不幸的是我们需要突破以降低 运行 API 的大量调用速度,因此我们必须进行并行处理。

正如预期的那样,我们 运行 在我们进行的第二次数据库调用中遇到了 DbContext 应急问题:

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.

我们read that a new UOW is required,所以我们尝试同时使用方法属性和显式 UowManager,但两者都不起作用。

我们还尝试使用 IocResolver 创建引用的 AppServices 的实例,但我们仍然无法为每个线程获得唯一的 DbContext(请参见下文)。

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;
}

我们如何确保每个线程都有一个新的 DbContext?

我能够使用通用版本的 ABP 复制并解决问题。我在原来的解决方案中仍然遇到这个问题,它要复杂得多。我将不得不做更多的挖掘以确定它在那里失败的原因。

对于遇到此问题的其他人,这与参考 here 完全相同的问题,您可以简单地通过属性禁用 UnitOfWork,如下面的代码所示。

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;
    }
}