[ASP.NET Core 3框架揭秘]服務承載系統[3]:總體設計[上篇]

前面的實例演示了服務承載的基本編程模式,接下來我們從設計的角度來重新認識服務承載模型。總的來說,服務承載模型主要由如下圖所示的三個核心對象組成:多個通過IHostedService接口表示的服務被承載於通過IHost接口表示的宿主上,IHostBuilder接口表示IHost對象的構建者。

10-7

一、IHostedService

承載的服務總是會被定義成IHostedService接口的實現類型。如下面的代碼片段所示,該接口僅定義了兩個用來啟動和關閉自身服務的方法。當作為宿主的IHost對象被啟動的時候,它會利用依賴注入框架激活每個註冊的IHostedService服務,並通過調用StartAsync方法來啟動它們。當服務承載應用程序關閉的時候,作為服務宿主的IHost對象會被關閉,由它承載的每個IHostedService服務對象的StopAsync方法也隨之被調用。

public interface IHostedService  {      Task StartAsync(CancellationToken cancellationToken);      Task StopAsync(CancellationToken cancellationToken);  }

承載系統無縫集成了.NET Core的依賴注入框架,在服務承載過程中所需的依賴服務,包括承載服務自身和它所依賴的服務均由此框架提供,承載服務註冊的本質就是將對應的IHostedService實現類型或者實例註冊到依賴注入框架中。由於承載服務大都需要長時間運行直到應用被關閉,所以針對承載服務的註冊一般採用Singleton生命周期模式。承載系統為承載服務的註冊定義了如下這個AddHostedService<THostedService>擴展方法。由於該方法通過調用TryAddEnumerable擴展方法來註冊服務,所以不用擔心服務重複註冊的問題。

public static class ServiceCollectionHostedServiceExtensions  {      public static IServiceCollection AddHostedService<THostedService>(this IServiceCollection services) where THostedService: class, IHostedService      {          services.TryAddEnumerable(ServiceDescriptor.Singleton<IHostedService, THostedService>());          return services;      }  }

二、IHost

通過IHostedService接口表示的承載服務最終被承載於通過IHost接口表示的宿主上。一般來說,一個服務承載應用在整個生命周期內只會創建一個IHost對象,我們啟動和關閉應用程序本質上就是啟動和關閉作為宿主的IHost對象。如下面的代碼片段所示,IHost接口派生於IDisposable接口,所以當它在關閉之後,應用程序還會調用其Dispose方法作一些額外的資源釋放工作。IHost接口的Services屬性返回作為依賴注入容器的IServiceProvider對象,該對象提供了服務承載過程中所需的服務實例,其中就包括需要承載的IHostedService服務。定義在IHost接口中的StartAsync和StopAsync方法完成了針對服務宿主的啟動和關閉。

public interface IHost : IDisposable  {      IServiceProvider Services { get; }      Task StartAsync(CancellationToken cancellationToken = default);      Task StopAsync(CancellationToken cancellationToken = default);  }

三、應用生命周期

在前面演示的實例中,在利用HostBuilder對象構建出IHost對象之後,我們並沒有調用其StartAsync方法啟動它,而是另一個名為Run的擴展方法。Run方法涉及到服務承載應用生命周期的管理,如果想了解該方法的本質,就得先來認識一個名為IHostApplicationLifetime的接口。顧名思義,IHostApplicationLifetime接口體現了服務承載應用程序的生命周期。如下面的代碼片段所示,該接口除了提供了三個CancellationToken類型的屬性來檢測應用何時開啟與關閉之外,還提供了一個StopApplication來關閉應用程序。

public interface IHostApplicationLifetime  {      CancellationToken ApplicationStarted { get; }      CancellationToken ApplicationStopping { get; }      CancellationToken ApplicationStopped { get; }        void StopApplication();  }

如下所示的ApplicationLifetime類型是對IHostApplicationLifetime接口的默認實現。我們可以看到它實現的三個屬性返回的CancellationToken對象來源於三個對應的CancellationTokenSource對象,後者對應着三個不同的方法(NotifyStarted、StopApplication和NotifyStopped)。我們可以利用IHostApplicationLifetime服務的三個屬性提供的CancellationToken對象得到關於應用被啟動和關閉通知,這些通知最初就是由這三個對應的方法發出來的。

public class ApplicationLifetime : IHostApplicationLifetime  {      private readonly ILogger<ApplicationLifetime> _logger;      private readonly CancellationTokenSource _startedSource;      private readonly CancellationTokenSource _stoppedSource;      private readonly CancellationTokenSource _stoppingSource;        public ApplicationLifetime(ILogger<ApplicationLifetime> logger)      {          _startedSource = new CancellationTokenSource();          _stoppedSource = new CancellationTokenSource();          _stoppingSource = new CancellationTokenSource();          _logger = logger;      }        private void ExecuteHandlers(CancellationTokenSource cancel)      {          if (!cancel.IsCancellationRequested)          {              cancel.Cancel(false);          }      }        public void NotifyStarted()      {          try          {              this.ExecuteHandlers(this._startedSource);          }          catch (Exception exception)          {              _logger.ApplicationError(6, "An error occurred starting the application",exception);          }      }        public void NotifyStopped()      {          try          {              ExecuteHandlers(this._stoppedSource);          }          catch (Exception exception)          {              _logger.ApplicationError(8, "An error occurred stopping the application",exception);          }      }        public void StopApplication()      {          CancellationTokenSource source = this._stoppingSource;          lock (source)          {              try              {                  ExecuteHandlers(this._stoppingSource);              }              catch (Exception exception)              {                  _logger.ApplicationError(7, "An error occurred stopping the application", exception);              }          }      }        public CancellationToken ApplicationStarted => _startedSource.Token;      public CancellationToken ApplicationStopped => _stoppedSource.Token;      public CancellationToken ApplicationStopping => _stoppingSource.Token;  }

四、利用IHostApplicationLifetime關閉應用

我們接下來通過一個簡單的實例來演示如何利用IHostApplicationLifetime服務來關閉整個承載應用程序。我們在一個控制台應用程序中定義了如下這個承載服務FakeHostedService。在FakeHostedService類型的構造函數中,我們注入了IHostApplicationLifetime服務。在得到其三個屬性返回的CancellationToken對象之後,我們在它們上面分別註冊了一個回調,回調操作通過在控制台上輸出相應的文字使我們可以知道應用程序何時被啟動和關閉。

public sealed class FakeHostedService : IHostedService  {      private readonly IHostApplicationLifetime _lifetime;      private IDisposable _tokenSource;        public FakeHostedService(IHostApplicationLifetime lifetime)      {          _lifetime = lifetime;          _lifetime.ApplicationStarted.Register(() => Console.WriteLine("[{0}]Application started", DateTimeOffset.Now));          _lifetime.ApplicationStopping.Register(() => Console.WriteLine("[{0}]Application is stopping.", DateTimeOffset.Now));          _lifetime.ApplicationStopped.Register(() => Console.WriteLine("[{0}]Application stopped.", DateTimeOffset.Now));      }        public Task StartAsync(CancellationToken cancellationToken)      {          _tokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(5)).Token.Register(_lifetime.StopApplication);          return Task.CompletedTask;      }        public Task StopAsync(CancellationToken cancellationToken)      {          _tokenSource?.Dispose();          return Task.CompletedTask;      }  }

在實現的StartAsync方法中,我們採用如上的方式在5秒之後調用IHostApplicationLifetime服務的StopApplication方法來關閉整個應用程序。這個FakeHostedService服務最後採用如下的方式承載於當前應用程序中。

class Program  {      static void Main()      {          new HostBuilder()              .ConfigureServices(svcs => svcs.AddHostedService<FakeHostedService>())              .Build()              .Run();      }  }

該程序運行之後會在控制台上輸出如下圖所示的結果,從三條消息產生的時間間隔我們可以確定當前應用程序正是承載FakeHostedService通過調用IHostApplicationLifetime服務的StopApplication方法關閉的。(源代碼從這裡下載)

10-8

五、Run擴展方法

如果我們調用IHost對象的擴展方法Run,它會在內部調用StartAsync方法,接下來它會持續等待下去直到接收到應用被關閉的通知。當IHost對象對象利用IHostApplicationLifetime服務接收到關於應用關閉的通知後,它會調用自身的StopAsync方法,針對Run方法的調用此時才會返回。啟動IHost對象直到應用關閉這一實現體現在如下這個WaitForShutdownAsync擴展方法上。

public static class HostingAbstractionsHostExtensions  {      public static async Task WaitForShutdownAsync(this IHost host, CancellationToken token = default)      {          var applicationLifetime = host.Services.GetService<IHostApplicationLifetime>();          token.Register(state => ((IHostApplicationLifetime)state).StopApplication(), applicationLifetime);            var waitForStop = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);          applicationLifetime.ApplicationStopping.Register(state =>          {              var tcs = (TaskCompletionSource<object>)state;              tcs.TrySetResult(null);          }, waitForStop);            await waitForStop.Task;          await host.StopAsync();      }  }

如下所示的WaitForShutdown方法是上面這個WaitForShutdownAsync方法的同步版本。同步的Run方法和異步的RunAsync方法的實現也體現在下面的代碼片段中。除此之外,下面的代碼片段還提供了Start和StopAsync這兩個擴展方法,前者可以視為StartAsync方法的同步版本,後者可以在關閉IHost對象的時候指定一個超時時限。

public static class HostingAbstractionsHostExtensions  {      public static void WaitForShutdown(this IHost host) => host.WaitForShutdownAsync().GetAwaiter().GetResult();      public static void Run(this IHost host) => host.RunAsync().GetAwaiter().GetResult();      public static async Task RunAsync(this IHost host, CancellationToken token = default)      {          try          {              await host.StartAsync(token);              await host.WaitForShutdownAsync(token);          }          finally          {              host.Dispose();          }      }      public static void Start(this IHost host) => host.StartAsync().GetAwaiter().GetResult();      public static Task StopAsync(this IHost host, TimeSpan timeout) => host.StopAsync(new CancellationTokenSource(timeout).Token);  }

服務承載系統[1]: 承載長時間運行的服務[上篇]
服務承載系統[2]: 承載長時間運行的服務[下篇]
服務承載系統[3]: 總體設計[上篇]
服務承載系統[4]: 總體設計[下篇]
服務承載系統[5]: 承載服務啟動流程[上篇]
服務承載系統[6]: 承載服務啟動流程[下篇]