【问题标题】:How do I handle async operations in Startup.Configure?如何在 Startup.Configure 中处理异步操作?
【发布时间】:2015-11-22 07:46:12
【问题描述】:

在我的 ASP.NET 5 应用程序中,我想将一些数据从 Azure 加载到我的 Startup.Configure 方法内的缓存中。 Azure SDK 专门公开异步方法。通常,调用异步方法是通过异步方法中的 await 完成的,如下所示:

public async Task Configure(IApplicationBuilder app, IMemoryCache cache)
{
    Data dataToCache = await DataSource.LoadDataAsync();
    cache.Set("somekey", dataToCache);

    // remainder of Configure method omitted for clarity
}

但是,ASP.NET 5 要求 Configure 方法返回 void。我可以使用 async void 方法,但我的理解是 async void 方法只应该用于事件处理程序(根据https://msdn.microsoft.com/en-us/magazine/jj991977.aspx 等等)。

我在想一个更好的方法是在没有等待的情况下调用异步函数,在返回的任务上调用等待,然后通过 Task.Results 属性缓存结果,如下所示:

public void Configure(IApplicationBuilder app, IMemoryCache cache)
{
    Task<Data> loadDataTask = DataSource.LoadDataAsync();
    loadDataTask.Wait();
    cache.Set("somekey", loadDataTask.Result);

    // remainder of Configure method omitted for clarity
}

Stephen Walther 今年早些时候在blog post 中使用了类似的方法。但是,从该帖子中不清楚这是否被认为是可接受的做法。是吗?

如果这被认为是一种可接受的做法,我需要什么(如果有的话)错误处理?我的理解是 Task.Wait() 将重新抛出异步操作抛出的任何异常,并且我没有提供任何机制来取消异步操作。简单地调用 Task.Wait() 就足够了吗?

【问题讨论】:

  • 这是一个有趣的问题。我不知道,但我认为 在这种情况下 做 async void 没有副作用。希望有经验的大侠解答一下。
  • async void 确实 在这种情况下会产生副作用,因为它会向调用该方法的任何人发出信号,表明他们可以在该方法运行时继续进行其他工作。由于Configure 处理应用程序设置,因此在完成之前继续运行应用程序代码的其他部分可能会产生不可预知的后果。 (我们遇到了这个问题,它使我们的应用程序变得混乱,因为依赖注入器注入了尚未正确设置的服务)。
  • 这是另一种方法,您可以在网络服务器运行之前运行初始化代码。 stackoverflow.com/a/55707949/1912383

标签: c# asp.net-core async-await


【解决方案1】:

你可以做一些异步工作,但方法是同步的,你不能改变它。这意味着您需要同步等待异步调用完成。

如果启动尚未完成,您不想从 Startup 方法返回,对吗?您的解决方案似乎没问题。

关于异常处理:如果没有某项工作您的应用程序无法正常运行,您应该让 Startup 方法失败(请参阅Fail-fast)。如果不是关键问题,我会将相关部分包含在 try catch 块中,并记录问题以供以后检查。

【讨论】:

    【解决方案2】:

    您链接到的博客中的示例代码仅使用异步同步来使用示例数据填充数据库;该调用不会存在于生产应用中。

    首先,我想说,如果您确实需要 Configure 是异步的,那么您应该向 ASP.NET 团队提出问题,以便他们关注。此时(即在发布之前)添加对ConfigureAsync 的支持对他们来说并不难。

    其次,您有几种解决问题的方法。您可以使用task.Wait(或者更好的是task.GetAwaiter().GetResult(),如果确实发生错误,它可以避免AggregateException 包装器)。或者,您可以缓存 task 而不是任务的 result (如果 IMemoryCache 更像是一个字典而不是一些奇怪的 serialize-into-binary-array -内存中的东西 - 我在看着你,以前版本的 ASP.NET)。

    如果这被认为是一种可接受的做法,我需要什么(如果有的话)错误处理?

    使用GetAwaiter().GetResult() 会导致异常(如果有)从Configure 传播出去。不过,如果配置应用程序失败,我不确定 ASP.NET 会如何响应。

    我没有提供任何机制来取消异步操作。

    我不确定如何“取消”应用程序的设置,所以我不会担心这部分。

    【讨论】:

    • 关于您的第一点,过去几年 ASP.NET 团队提出了各种问题。目前的版本似乎是Issue #1088,并且不会比 v3.0 更早被采用,它还没有出现在 the roadmap 上,这意味着不会早于 2018 年的某个时间。
    • 我发现在没有返回任何内容时仅使用GetAwaiter() 是不够的。您必须使用 GetAwaiter().GetResult() 才能“完成”异步操作。
    • @user247702 你是对的,它看起来像。他们最终在 3.x 中为此添加了更好的支持。我在下面添加了一个答案,展示了利用这些新功能的一种方法。
    • 这是跟踪它的新问题github.com/dotnet/aspnetcore/issues/24142。看起来它也不会为 NET6 做好准备:(
    【解决方案3】:

    如果您的异步代码进行进一步的异步调用,特别是如果这些是回调,那么这里的答案并不总是正确工作,那么您可能会发现代码死锁。

    这对我来说发生过很多次,并且使用Nito.AsyncEx 效果很好。

    using Nito.AsyncEx;
    
    AsyncContext.Run(async () => { await myThing.DoAsyncTask(); });
    

    【讨论】:

    【解决方案4】:

    Dotnet Core 3.x 对此提供了更好的支持。

    首先,您可以为缓存过程创建一个类。让它实现IHostedService,如下所示。只需实现两个功能:

        private readonly IServiceProvider _serviceProvider;
        public SetupCacheService(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
        }
    
        public async Task StartAsync(CancellationToken cancellationToken)
        {
            // Perform your caching logic here. 
            // In the below example I omit the caching details for clarity and 
            // instead show how to get a service using the service provider scope. 
            using (var scope = _serviceProvider.CreateScope())
            {
                // Example of getting a service you registered in the startup
                var sampleService = scope.ServiceProvider.GetRequiredService<IYourService>();
    
                // Perform the caching or database or whatever async work you need to do. 
                var results = sampleService.DoStuff();
                var cacheEntryOptions = new MemoryCacheEntryOptions(){ // cache options };
    
                // finish caching setup..
            }
        }
    
        public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
    

    现在,Starup.cs

        public virtual void ConfigureServices(IServiceCollection services)
        {
            // Normal service registration stuff. 
            // this is just an example. There are 1000x ways to do this. 
            services.AddTransient(IYourService, ConcreteService);
    
           // Here you register the async work from above, which will 
           // then be executed before the app starts running
           services.AddHostedService<SetupCacheService>();
        }
    

    就是这样。 请注意,我对此的解决方案很大程度上依赖于Andrew Lock's article。我非常感谢他抽出时间来写这篇文章。

    来自我由 Andrew Lock 发布的链接,

    服务将在启动时按照它们添加到 DI 容器的顺序执行,即稍后在 ConfigureServices 中添加的服务将在启动时稍后执行。

    希望这对寻找 Dotnet core 3.x+ 方法的人有所帮助。

    【讨论】:

    • 感谢@joshmcode。我们可以在 StartAsync 中注册单例服务吗?我基本上想注册一个作为全局应用程序设置的单例,需要从数据库中异步提取数据,这就是为什么我在 Startup 中查看异步操作路径的原因。我考虑过通过您详述的托管服务缓存数据,并让单例从缓存中提取,但我担心如果缓存过期会导致问题。因此,如果我们可以在那里注册单例,那将是理想的。
    • @AnimaSola 我相信这是可能的,但它可能有点棘手。您可以采取的一种可能方法是结合服务注入异步更新Writable 设置。虽然我不知道您的解决方案的完整规格,但我可以想象您在 HostedService 中注入 IServiceProvider 并让您的服务访问数据库。您也可以注入IWritableOptions&lt;T&gt; 并更新选项。然后那些IWritableOptions 在整个应用程序中都可用。我还没有完全尝试过,但我已经分别完成了这两个步骤。
    • 这种方法的问题是当托管服务开始执行时服务已经启动。如果您依赖于在应用程序初始化之前完成的工作,这可能会导致问题,这就是我的情况。由于托管服务开始运行应用程序按定义启动后,它们不能在您需要它作为一部分初始化的情况下使用。
    • @julealgon 你是对的,这不是它的目的。这种方法旨在在不阻塞的情况下启动异步进程。当应用程序启动时,我使用上述方法启动后台进程。我不知道 OP 是否真的指定该过程必须首先完成。他们只是问如何在启动时启动异步进程。根据定义,异步进程不是您所描述的正确方法。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-10-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-07-18
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多