如何在可取消请求流中重复使用单个异步Web服务调用?

时间:2021-10-25 12:20:52

In my WPF UI, I have a list of customers. I also have a Web API service for fetching a single customer's profile. Both the server side and the client side are built using async/await and are cancellable.

在我的WPF UI中,我有一个客户列表。我还有一个Web API服务,用于获取单个客户的个人资料。服务器端和客户端都使用async / await构建并且是可取消的。

When the user selects "Customer A" from the ComboBox, it triggers the server call to fetch the customer profile. After 2 seconds (the contrived duration of the server action method) the data returns and is displayed. If, during that 2 seconds "Customer B" is selected, my code cancels the first request and fires off the second request.

当用户从ComboBox中选择“Customer A”时,它会触发服务器调用以获取客户配置文件。 2秒后(服务器操作方法的设计持续时间),数据返回并显示。如果在2秒内选择“客户B”,我的代码将取消第一个请求并触发第二个请求。

This all works great, the is-busy/cancellation logic is fairly naive and doesn't cancel properly if the user very quickly selects different customers. For example, if I press the down arrow key (with the combobox having focus) 10 times quickly, 5 of the requests are correctly cancelled on the server side and 5 of the requests are not. While this isn't the end of the world, I don't want to chew up server resources with potentially big database queries running in parallel for no reason.

这一切都很有效,忙碌/取消逻辑非常幼稚,如果用户非常快速地选择不同的客户,则不能正确取消。例如,如果我快速按下向下箭头键(组合框具有焦点)10次,则在服务器端正确取消5个请求,而不是5个请求。虽然这不是世界末日,但我不想在没有任何理由的情况下并行运行大型数据库查询来咀嚼服务器资源。

Here is my client code:

这是我的客户端代码:

CancellationTokenSource _customerProfileCts;

//called when a new customer item is selected from the UI's ComboBox
private async void TriggerGetCustomerProfile()
{
    //if there is already a customer profile fetch operation in progress, we just want to cancel it and start a new one. 
    if (IsBusyFetchingCustomerProfile)
    {
        _customerProfileCts.Cancel();
    }

    try
    {
        IsBusyFetchingCustomerProfile = true;
        IsCustomerProfileReady = false;
        await GetCustomerProfile();
    }
    finally
    {
        IsBusyFetchingCustomerProfile = false;
        _customerProfileCts = null;
    }
}

private async Task GetCustomerProfile()
{
    _customerProfileCts = new CancellationTokenSource();
    await _customerSvc.GetCustomerProfileReport(SelectedCustomer.Id, _customerProfileCts.Token);
    //logic for checking result of web call and distributing received data omitted
}

I feel like there should be some well-established pattern for this kind of thing, that ensures that every single request is cancelled if a new UI items is selected, no matter how fast the user selects.

我觉得应该为这种事情建立一些完善的模式,确保在选择新的UI项目时取消每个请求,无论用户选择多快。

In fact, isn't this one of the highlighted use cases for reactive extensions? I see lots of mention of hypothetical problems like calling a search web service after keystrokes are entered in a textbox, but I have not found any example that deals with cancelling previously sent requests.

事实上,这不是反应性扩展的突出用例之一吗?我看到很多提到假设的问题,例如在文本框中输入击键后调用搜索Web服务,但我没有找到任何处理取消先前发送的请求的示例。

I just need something clean and rock-solid, hopefully something that can be packaged up and hide the complexities so that this kind of cancellable-async-fetching can be used elsewhere in my app trouble-free.

我只需要一些干净且坚如磐石的东西,希望能够打包并隐藏复杂性的东西,以便这种可取消的异步提取可以在我的应用程序的其他地方无故障地使用。

1 个解决方案

#1


3  

Your problem is that your finally is modifying _customerProfileCts, which can null out instances that are still in use by other calls. If you move that to after the Cancel, then it should work fine. In fact, you can combine it with the modification in GetCustomerProfile as such:

您的问题是您最终正在修改_customerProfileCts,这可能会使其他调用仍在使用的实例无效。如果你把它移到取消后,它应该工作正常。实际上,您可以将它与GetCustomerProfile中的修改结合起来:

CancellationTokenSource _customerProfileCts;

private async void TriggerGetCustomerProfile()
{
  if (_customerProfileCts != null)
  {
    _customerProfileCts.Cancel();
  }
  _customerProfileCts = new CancellationTokenSource();
  var token = _customerProfileCts.Token;

  try
  {
    IsBusyFetchingCustomerProfile = true;
    IsCustomerProfileReady = false;
    await GetCustomerProfile(token);
  }
  finally
  {
    IsBusyFetchingCustomerProfile = false;
  }
}

private async Task GetCustomerProfile(CancellationToken token)
{
  await _customerSvc.GetCustomerProfileReport(SelectedCustomer.Id, token);
}

#1


3  

Your problem is that your finally is modifying _customerProfileCts, which can null out instances that are still in use by other calls. If you move that to after the Cancel, then it should work fine. In fact, you can combine it with the modification in GetCustomerProfile as such:

您的问题是您最终正在修改_customerProfileCts,这可能会使其他调用仍在使用的实例无效。如果你把它移到取消后,它应该工作正常。实际上,您可以将它与GetCustomerProfile中的修改结合起来:

CancellationTokenSource _customerProfileCts;

private async void TriggerGetCustomerProfile()
{
  if (_customerProfileCts != null)
  {
    _customerProfileCts.Cancel();
  }
  _customerProfileCts = new CancellationTokenSource();
  var token = _customerProfileCts.Token;

  try
  {
    IsBusyFetchingCustomerProfile = true;
    IsCustomerProfileReady = false;
    await GetCustomerProfile(token);
  }
  finally
  {
    IsBusyFetchingCustomerProfile = false;
  }
}

private async Task GetCustomerProfile(CancellationToken token)
{
  await _customerSvc.GetCustomerProfileReport(SelectedCustomer.Id, token);
}