使用事件处理程序时抛出异常 "A second operation started on this context before a previous operation completed"

Exception "A second operation started on this context before a previous operation completed" thrown when using event handler

我的 C# 控制台应用程序项目中有一个事件处理程序,当来自用户的更新到达时触发(聊天机器人场景)。问题是,即使我在事件处理程序中使用 await,当代码到达要从数据库中获取用户数据的位置时,我仍然会遇到异常(无效操作异常)。

另一方面,当我放弃事件处理程序并使用长轮询技术获取更新时,我没有遇到这个问题。我在想也许这个事件处理程序为它收到的每个更新创建一个新线程,所以这就是抛出这个异常的原因。我想知道我是否可以使用事件处理程序而不面临这个问题?这是我的代码:

 public class TelegramService : IChatbotService
    {
        private readonly ILogger _logger;
        private readonly ITelegramBotClientFactory _telegramBotFactory;
        private ITelegramBotClient _telegramBotClient;
        internal static User Me;
        private readonly IChatbotUpdateHandler _chatbotUpdateHandler;
        private readonly ISettingService _settingService;

        public TelegramService(ITelegramBotClientFactory telegramBotClientFactory, ILogger<TelegramService> logger,
            IChatbotUpdateHandler chatbotUpdateHandler)
        {
            _logger = logger;
            _telegramBotFactory = telegramBotClientFactory;
            _chatbotUpdateHandler = chatbotUpdateHandler;

        }
        public async Task<bool> Run()
        {

            try
            {
                _telegramBotClient = _telegramBotFactory.CreateBotClient();

                await _telegramBotClient.DeleteWebhookAsync();


                Me = await _telegramBotClient.GetMeAsync();
            }
            catch (Exception e)
            {
                _logger.LogError($"502 bad gateway, restarting in 2 seconds:\n\n{e.Message}", e.Message);
                Thread.Sleep(TimeSpan.FromSeconds(2));
                //API is down... 
                return true;
            }
          
            _telegramBotClient.OnUpdate += BotOnUpdateReceived; // event handler

            _telegramBotClient.StartReceiving();

            return false;
        }

  private async void BotOnUpdateReceived(object sender, UpdateEventArgs args)
        {
            var update = args.Update;
            if (update.Type == UpdateType.InlineQuery) return;
            if (update.Type == UpdateType.CallbackQuery) return;

            await _chatbotUpdateHandler.Handle(update);

        }

}

public class TelegramUpdateHandler : IChatbotUpdateHandler
    {

        private Update _update;
        private readonly ILogger<TelegramUpdateHandler> _logger;
        private readonly IUserService _userService;
        private readonly IChatProcessorFactory _chatProcessorFactory;
        private readonly IUserMessagingService _userMessagingService;

        public TelegramUpdateHandler(ILogger<TelegramUpdateHandler> logger, IUserService userService,
            IChatProcessorFactory chatProcessorFactory, IUserMessagingService userMessagingService)
        {
            _logger = logger;
            _userService = userService;
            _chatProcessorFactory = chatProcessorFactory;
            _userMessagingService = userMessagingService;
        }
        public async Task Handle(object updateObject)
        {
            
            try
            {
                var botUser = await GetUser();
               
                await ProcessUpdate(botUser);


            }
            catch (UnAuthorizedException e)
            {
                //User is grounded or does not have access to bot
                _logger.LogInformation($"User is unauthorized to access the bot:\n{e.Message}");
            }
            catch (Exception e)
            {
                _logger.LogError($"Error occured at Handle:\n{e.Message}");
            }

        }

        private async Task<BotUser> GetUser()
        {
            BotUser botUser = null;

            try
            {
                botUser = await _userService.FetchUser(_update.Message.From.Id);
                //Exception is thrown when calling "FetchUser" when second update comes here.
            }
            catch (InvalidOperationException e)
            {
                botUser = _userService.CreateNewBotUser(_update.Message.From);

                botUser = await _userService.AddUserToDb(botUser);
            }

            return botUser;
        }
    }
    

    [Export(typeof(IUserService))]
    public class UserService : IUserService
    {
        private readonly ILanguageService _languageService;
        private readonly IUnitOfWork _unitOfWork;
        private readonly ITelegramApiService _telegramApiService;

        [ImportingConstructor]
        public UserService(ITelegramApiService telegramApiService, ILanguageService languageService, IUnitOfWork unitOfWork)
        {
            _telegramApiService = telegramApiService;
            _languageService = languageService;
            _unitOfWork = unitOfWork;
        }

        public async Task<BotUser> FetchUser(int userId)
        {

            return await _unitOfWork.Users.Fetch(userId);

        }
     }

如果我将上面的内容更改为如下所示,我不会遇到任何问题:

var updates = await GetUpdates(); 
//Through long polling we get the updates rather than using an event handler.

            if (updates.Length > 0)
              {
                  HandleUpdates(updates.ToList());   

                  lastUpdateID = updates[^1].Id;
              }

///........

private async Task HandleUpdates(List<Update> updates)
        {
            foreach (var item in updates)
            {
                if (item.Type == UpdateType.InlineQuery) continue;
                if (item.Type == UpdateType.CallbackQuery) continue;

                await _chatbotUpdateHandler.Handle(item);

            }
        }

/// The rest is similar to the previous version

PS*: 我也已将我所有的服务注册为 Transient

event handler in my C# console app project

async void was designed for event handlers, but in a way that assumes they're like UI event handlers:

  1. await 捕获当前上下文并在该上下文中继续。
  2. async void 方法引发的异常在方法开头出现的 SynchronizationContext 上重新引发。
  3. 没有办法 await 一个 async void 方法; UI 只是 returns 到它的消息循环。

在 UI 世界中,这两种行为都有意义,并导致 async void 事件处理程序与非 async 事件处理程序具有相似的语义。在控制台世界中,这些 行为导致这些语义(分别):

  1. 在每个 await 之后,处理程序将继续在线程池线程上执行。
  2. async void 方法引发的异常直接在线程池上重新引发,这会使进程崩溃。
  3. 无法 await 一个 async void 方法,因此您的代码不能(轻易地)知道它何时完成。

因此,async 控制台进程中的事件处理不像在 UI 框架中那样有效。也就是说,如果您愿意,您可以仍然使用它们;你只需要知道这些语义。

具体来说,由于 await 将在线程池线程上恢复,为了避免“第二次操作”异常,您需要:

  1. 为每个事件处理程序提供一个单独的 DbContext 实例。
  2. Change the event handler to support (asynchronous) notifications that the event handlers are complete(例如,使用延迟)。
  3. 重构代码,以便将事件放入由 BackgroundService 组件处理的队列中(例如 Channel<T>)。

使用第一种方法的示例(为每个处理程序创建一个新的 DbContext):

private async void BotOnUpdateReceived(object sender, UpdateEventArgs args)
{
  var update = args.Update;
  if (update.Type == UpdateType.InlineQuery) return;
  if (update.Type == UpdateType.CallbackQuery) return;

  var chatbotUpdateHandler = _serviceProvider.GetRequiredService<IChatbotUpdateHandler>();
  await chatbotUpdateHandler.Handle(update);
}