【问题标题】:Save to DB after response was already sent (.NET Core & EF Core)发送响应后保存到数据库(.NET Core 和 EF Core)
【发布时间】:2020-07-28 07:55:31
【问题描述】:

我有一个使用 Entity Framework Core 的 .Net Core 3.1 Web API。它使用 HTTP Polling (https://docs.microsoft.com/en-us/azure/architecture/patterns/async-request-reply),因此可以调用一个端点来开始一个进程,另一个端点返回进程的状态,另一个端点返回结果。简而言之,第一个调用将很快返回,但会启动一个在返回后完成的过程。当这个过程完成时,我需要将一些东西保存到数据库中,但我的 dbContext 已经被处理掉了。

这是完整的细节:

Controller 操作将要求注入的服务启动进程(A 计算),然后它会返回端点以调用以获取有关该进程的更多信息:

[HttpPost]
public ActionResult<CalculationResource> createNewCalculationRequest([FromBody, BindRequired] CalculationRequestResource calculationRequest)
{
    var calc = _service.newCalculation(calculationRequest.Formats, calculationRequest.ParticipantId, calculationRequest.CaseId, calculationRequest.Input);
    var calcResponse = _mapper.Map<CalculationResource>(calc);
    return CreatedAtAction(nameof(getCalculation), new { calculationId = calc.Id }, calcResponse);
}

该服务将保存该计算,然后要求驱动程序启动它。驱动程序将同步返回进程已启动(或未启动)。此时响应已发送,但当驱动程序被要求启动计算时,它被赋予了一个在进程完成时调用的函数,该函数更新状态并设置结果。此函数将在响应已发送后执行。

这是服务代码:

    public CalculationService(CalculationsContext context, ICalculationDriver driver, ILogger<CalculationService> logger)
    {
        this._context = context;
        this._driver = driver;
        this._logger = logger;
    }     

    public Calculation newCalculation(ISet<Format> requestedFormats, string participantId, string caseId, string input)
    {
        var now = DateTime.Now;
        var newCalcId = CreateNewCalculationId(caseId, participantId, now);
        var newCalc = new Calculation(newCalcId, caseId, participantId, requestedFormats, now);
        var newInput = new CalculationInput(newCalcId, input);
        _context.Calculations.Add(newCalc);
        _context.CalculationInputs.Add(newInput);
        foreach(Format format in requestedFormats)
        {
            var result = new CalculationResult(newCalcId, format, Status.Needed, null);
            _context.CalculationResults.Add(result);
        }
        
        var calcStartedInfos = _driver.LaunchCalculation(newCalc.Id, input, requestedFormats, (statusUpdate) => {
            //Everything in here is executed later. It is a function passed to the driver which the driver calls to alert this service of status updates
            try
            {
                //Results and Status exist on the same entity, so we might as well grab it once, up here
                var resultEntity = _context.CalculationResults.Find(newCalcId, statusUpdate.Format);

                //If it completed successfully, we need to save the results
                if (statusUpdate.Status == Status.CompletedOk)
                {
                    //Get the results from the driver
                    using Stream resultStream = _driver.RetrieveResults(statusUpdate.CalculationId, statusUpdate.Format);
                    using CryptoStream resultStreamEncoded = new CryptoStream(resultStream, new ToBase64Transform(), CryptoStreamMode.Read);
                    using StreamReader sr = new StreamReader(resultStreamEncoded);
                    String result = sr.ReadToEnd();

                    //Update the repo with them
                    resultEntity.Result = result;
                }

                //Save the status, whatever it is
                _logger.LogDebug("Updating status for calculation {calcId} format {format} to {status}", statusUpdate.CalculationId, statusUpdate.Format, statusUpdate.Status);
                resultEntity.Status = statusUpdate.Status;
                
                //Finally, persist changes to repo
                _context.SaveChanges();
            } catch (Exception e)
            {
                //If we don't eat an exception within this block and we allow it to bubble up, the entire process will crash
                _logger.LogError(e, "An exception occurred while handling a status update from the driver");
                try
                {
                    var resultEntity = _context.CalculationResults.Find(newCalcId, statusUpdate.Format);
                    resultEntity.Status = Status.CompletedErrorsDetected;
                    _context.SaveChanges();

                } catch (Exception e2)
                {
                    _logger.LogError(e2, "Failed to update calculation status after encountering an exception while handling a status update");
                }
            }
        });

        foreach(CalculationStartedInfo startInfo in calcStartedInfos)
        {
            //Each startInfo represents one format, so let's grab the entity for that format
            var resultEntity = _context.CalculationResults.Find(newCalcId, startInfo.Format);
            if (startInfo.Started)
            {
                _logger.LogTrace("Detected Calculation process start successful for calcId: {calculationId} format: {format}", startInfo.CalculationId, startInfo.Format);
                //Set the status to indicate that the calculation has actually started
                resultEntity.Status = Status.ProceedingNormally;
            } else
            {
                _logger.LogError("Detected Calculation process start failed for calcId: {calculationId} format: {format}", startInfo.CalculationId, startInfo.Format);
                //Set the status to indicate that the calculation couldn't even start
                resultEntity.Status = Status.CompletedErrorsDetected;
            }
        }

        _context.SaveChanges();
        return newCalc;
    }

驱动程序处理与实际执行计算的 EXE 的通信。它为计算中的每个格式创建该 EXE 的新进程(稍微简化,但应该足够好)并附加一个退出处理程序,该处理程序调用服务传入的回调函数。这是代码(为 SO简化一些不相关的细节):

        public IEnumerable<CalculationStartedInfo> LaunchCalculation(string calculationId, string data, IEnumerable<Format> formats, Action<CalculationStatusUpdate> StatusChanged)
        {
            if (StatusChanged == null)
            {
                throw new ArgumentNullException(nameof(StatusChanged), "Status Update function must not be null");
            }

            /* We have to report the initial startup status synchronously because if we report it with a call to StatusChanged
             * it appears to create a race condition against the StatusChanged that is called on Exit */
            List<CalculationStartedInfo> startedReport = new List<CalculationStartedInfo>();

            foreach(Format format in formats)
            {
                //If we use "using" here, the process will be disposed of before the Exited handler is called
                Process p = new Process();
                try
                {
                    ProcessStartInfo startInfo = new ProcessStartInfo()
                    {
                        //Basically we're calling out to an EXE to do the calculations
                    };
                    p.StartInfo = startInfo;
                    p.EnableRaisingEvents = true;

                    p.Exited += (object sender, EventArgs e) =>
                    {
                        Process endingP = (Process)sender;
                        string calcId = endingP.StartInfo.Environment[FOO_BAR];
                        int actualPid = endingP.Id;
                        int exitCode = endingP.ExitCode;
                        endingP.Dispose();

                        _logger.LogDebug("Process {processId} for calculation id {calculationId} has terminated with code {exitCode}.", actualPid, calcId, exitCode);
                        if (exitCode == 0)
                        {
                            StatusChanged(calculationId, format, Status.CompletedOk);
                        }
                        else
                        {
                            StatusChanged(calculationId, format, Status.CompletedErrorsDetected);
                        }

                    };

                    bool isStarted = p.Start();
                    if (isStarted)
                    {
                        _logger.LogDebug("Started {processName} with process id {processId} for calculation id {calculationId}", p.ProcessName, p.Id, calculationId);
                        //Adds a CalculationStartedInfo that reports this Format was successfully started
                    }
                    else
                    {
                        _logger.LogError("Failed to start process");
                        //Adds a CalculationStartedInfo that reports this Format was NOT successfully started
                    }
                }
                catch (Exception e)
                {
                    _logger.LogError(e, "Exception while starting calculation process: " + e.Message);
                    p.Dispose();
                    throw e;
                }

            }

            return startedReport;
        }

重申问题:在回调内部使用dbContext时,响应已经发送后,由于已经被释放,所以抛出异常。

【问题讨论】:

    标签: c# .net-core entity-framework-core


    【解决方案1】:

    所以我在写问题时找到了答案,但我想我还是会发布它,以防它会帮助某人,或者其他人有更好的答案建议(我绝对相信这需要被重构)。

    我发现 4 年前的 THIS SO 答案很有希望,但即使我在我的服务中注入了 DbContextOptions 并尝试这样做

    var calcStartedInfos = _driver.LaunchCalculation(newCalc.Id, input, requestedFormats, (statusUpdate) => {
        //Everything in here is executed later. It is a function passed to the driver which the driver calls to alert this service of status updates
        try
        {
            using (var secondContext = new CalculationsContext(_dbContextOptions) {
    
            ... etc
    

    我仍然无法使用 secondContext。尝试使用已处置的对象时,我会遇到另一个异常,但这次提到的对象是 IServiceProvider。

    最终我找到了THIS 博客文章,这导致我找到了这个解决方案:

    CalculationService 构造函数:

    public CalculationService(CalculationsContext context, IServiceScopeFactory scopeFactory, ICalculationDriver driver, ILogger<CalculationService> logger)
        {
            this._context = context;
            this._driver = driver;
            this._logger = logger;
    
            /*To understand why we need this, see the callback within newCalculation*/
            this._scopeFactory = scopeFactory;
        }
    

    CalculationService 新的计算方法:

        public Calculation newCalculation(ISet<Format> requestedFormats, string participantId, string caseId, string input)
        {
            //yada yada
            
            var calcStartedInfos = _driver.LaunchCalculation(newCalc.Id, input, requestedFormats, (statusUpdate) => {
                //Everything in here is executed later. It is a function passed to the driver which the driver calls to alert this service of status updates
                try
                {
                    /* Since this code is executing after the response was already sent, we can't use the original dbContext. It was request-scoped and so is now disposed of.
                       Instead, we are going to ask the singleton "IServiceScopeFactory" for a new scope, and within that scope we'll create a new dbContext */
                    using (var scope = _scopeFactory.CreateScope())
                    {
                        var secondContext = scope.ServiceProvider.GetService<CalculationsContext>();
    
                        //Results and Status exist on the same entity, so we might as well grab it once, up here
                        var resultEntity = secondContext.CalculationResults.Find(newCalcId, statusUpdate.Format);
    
                        //If it completed successfully, we need to save the results
                        if (statusUpdate.Status == Status.CompletedOk)
                        {
                            //Get the results from the driver
                            using Stream resultStream = _driver.RetrieveResults(statusUpdate.CalculationId, statusUpdate.Format);
                            using CryptoStream resultStreamEncoded = new CryptoStream(resultStream, new ToBase64Transform(), CryptoStreamMode.Read);
                            using StreamReader sr = new StreamReader(resultStreamEncoded);
                            String result = sr.ReadToEnd();
    
                            //Update the repo with them
                            resultEntity.Result = result;
                        }
    
                        //Save the status, whatever it is
                        _logger.LogDebug("Updating status for calculation {calcId} format {format} to {status}", statusUpdate.CalculationId, statusUpdate.Format, statusUpdate.Status);
                        resultEntity.Status = statusUpdate.Status;
    
                        //Finally, persist changes to repo
                        secondContext.SaveChanges();
                    }
                } catch (Exception e)
                {
                    //yada yada
                }
            });
    
            foreach(CalculationStartedInfo startInfo in calcStartedInfos)
            {
                //yada yada
            }
    
            _context.SaveChanges();
            return newCalc;
        }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-03-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-12-13
      相关资源
      最近更新 更多