1313using Microsoft . Extensions . Logging ;
1414using Microsoft . Extensions . DependencyInjection ;
1515using Microsoft . Extensions . Hosting ;
16+ using Newtonsoft . Json . Linq ;
1617
1718namespace TelegramBot
1819{
1920 /// <summary>
2021 /// Telegram bot application.
2122 /// </summary>
22- public class BotApp : IBot
23+ public class BotApp : IBot , IHost
2324 {
25+ private bool _disposed = false ;
2426 private readonly ILogger < BotApp > _logger ;
2527 private readonly TelegramBotClient _client ;
2628 private readonly ServiceProvider _serviceProvider ;
2729 private IReadOnlyCollection < MethodInfo > _controllerMethods ;
30+ private readonly CancellationTokenSource _cancellationTokenSource ;
31+
32+ /// <summary>
33+ /// Gets the services configured for the program (for example, using <see cref="M:HostBuilder.ConfigureServices(Action<HostBuilderContext,IServiceCollection>)" />).
34+ /// </summary>
35+ public IServiceProvider Services => _serviceProvider ;
2836
2937 /// <summary>
3038 /// Creates a new instance of <see cref="BotApp"/>.
3139 /// </summary>
3240 /// <param name="client">Telegram bot client.</param>
3341 /// <param name="serviceProvider">Service provider.</param>
34- public BotApp ( TelegramBotClient client , ServiceProvider serviceProvider )
42+ /// <param name="cancellationTokenSource">Cancellation token source.</param>
43+ public BotApp ( TelegramBotClient client , ServiceProvider serviceProvider , CancellationTokenSource cancellationTokenSource )
3544 {
3645 _client = client ;
3746 _serviceProvider = serviceProvider ;
3847 _controllerMethods = new List < MethodInfo > ( ) ;
48+ _cancellationTokenSource = cancellationTokenSource ;
3949 _logger = serviceProvider . GetRequiredService < ILogger < BotApp > > ( ) ;
4050 }
4151
@@ -44,6 +54,7 @@ public BotApp(TelegramBotClient client, ServiceProvider serviceProvider)
4454 /// </summary>
4555 public IBot MapControllers ( )
4656 {
57+ CheckDisposed ( ) ;
4758 var types = Assembly . GetCallingAssembly ( ) . GetTypes ( ) ;
4859 List < Type > result = new List < Type > ( ) ;
4960 foreach ( var type in types )
@@ -62,13 +73,36 @@ public IBot MapControllers()
6273 /// <summary>
6374 /// Runs the bot.
6475 /// </summary>
65- /// <param name="token ">Cancellation token (optional).</param>
66- public void Run ( CancellationToken token = default )
76+ /// <param name="cancellationToken ">Cancellation token (optional).</param>
77+ public void Run ( CancellationToken cancellationToken = default )
6778 {
79+ var mergedToken = MergeTokens ( cancellationToken ) ;
80+ CheckDisposed ( ) ;
81+ StartAsync ( mergedToken ) . Wait ( ) ;
82+ try
83+ {
84+ Task . Delay ( - 1 , mergedToken ) . Wait ( ) ;
85+ }
86+ catch ( OperationCanceledException )
87+ {
88+ _logger . LogInformation ( "Bot stopped - no longer receiving updates." ) ;
89+ }
90+ }
91+
92+ /// <summary>
93+ /// Starts the <see cref="IHostedService" /> objects configured for the program.
94+ /// The application will run until interrupted or until <see cref="M:IHostApplicationLifetime.StopApplication()" /> is called.
95+ /// </summary>
96+ /// <param name="cancellationToken">Used to abort program start.</param>
97+ /// <returns>A <see cref="Task"/> that will be completed when the <see cref="IHost"/> starts.</returns>
98+ public async Task StartAsync ( CancellationToken cancellationToken = default )
99+ {
100+ var mergedToken = MergeTokens ( cancellationToken ) ;
101+ CheckDisposed ( ) ;
68102 try
69103 {
70104 var botUser = _client . GetMeAsync ( ) . Result ;
71- _client . StartReceiving ( UpdateHandler , ErrorHandler , cancellationToken : token ) ;
105+ _client . StartReceiving ( UpdateHandler , ErrorHandler , cancellationToken : mergedToken ) ;
72106 _logger . LogInformation ( "Bot '{botUser}' started - receiving updates." , botUser . Username ) ;
73107 }
74108 catch ( Exception ex )
@@ -83,33 +117,63 @@ public void Run(CancellationToken token = default)
83117 var hostedServices = _serviceProvider . GetServices < IHostedService > ( ) ;
84118 foreach ( var hostedService in hostedServices )
85119 {
86- hostedService . StartAsync ( token ) . Wait ( token ) ;
87- }
88- try
89- {
90- Task . Delay ( - 1 , token ) . Wait ( token ) ;
91- }
92- catch ( OperationCanceledException )
93- {
94- _logger . LogInformation ( "Bot stopped - no longer receiving updates." ) ;
120+ await hostedService . StartAsync ( mergedToken ) ;
95121 }
122+ }
123+
124+ /// <summary>
125+ /// Attempts to gracefully stop the program.
126+ /// </summary>
127+ /// <param name="cancellationToken">Used to indicate when stop should no longer be graceful.</param>
128+ /// <returns>A <see cref="Task"/> that will be completed when the <see cref="IHost"/> stops.</returns>
129+ public async Task StopAsync ( CancellationToken cancellationToken = default )
130+ {
131+ CheckDisposed ( ) ;
96132 _logger . LogInformation ( "Stopping hosted services..." ) ;
97133 var hostApplicationLifetime = _serviceProvider . GetRequiredService < IHostApplicationLifetime > ( ) ;
98134 hostApplicationLifetime . StopApplication ( ) ;
135+ var hostedServices = _serviceProvider . GetServices < IHostedService > ( ) ;
99136 foreach ( var hostedService in hostedServices )
100137 {
101- hostedService . StopAsync ( token ) . Wait ( token ) ;
138+ try
139+ {
140+ await hostedService . StopAsync ( cancellationToken ) ;
141+ _logger . LogInformation ( "Hosted service '{hostedService}' stopped." , hostedService . GetType ( ) . Name ) ;
142+ }
143+ catch ( Exception ex )
144+ {
145+ _logger . LogError ( ex , "Error occurred while stopping hosted service '{hostedService}'." , hostedService . GetType ( ) . Name ) ;
146+ }
102147 }
148+ _logger . LogInformation ( "Stopping bot updates..." ) ;
149+ _cancellationTokenSource . Cancel ( ) ;
150+ }
151+
152+ /// <summary>
153+ /// Disposes the bot.
154+ /// </summary>
155+ public void Dispose ( )
156+ {
157+ CheckDisposed ( ) ;
158+ GC . SuppressFinalize ( this ) ;
159+ _disposed = true ;
160+ }
161+
162+ private CancellationToken MergeTokens ( CancellationToken token )
163+ {
164+ return CancellationTokenSource . CreateLinkedTokenSource ( _cancellationTokenSource . Token , token ) . Token ;
103165 }
104166
105167 private Task ErrorHandler ( ITelegramBotClient client , Exception exception , CancellationToken token )
106168 {
169+ CheckDisposed ( ) ;
107170 _logger . LogError ( exception , "Error occurred while receiving updates." ) ;
108171 return Task . CompletedTask ;
109172 }
110173
111174 private async Task UpdateHandler ( ITelegramBotClient client , Update update , CancellationToken token )
112175 {
176+ CheckDisposed ( ) ;
113177 if ( update . Message != null && ! string . IsNullOrWhiteSpace ( update . Message . Text ) && update . Message . Text . StartsWith ( '/' ) )
114178 {
115179 _logger . LogInformation ( "Received text message: {Text}." , update . Message . Text ) ;
@@ -130,6 +194,7 @@ private async Task UpdateHandler(ITelegramBotClient client, Update update, Cance
130194
131195 private async Task HandleRequestAsync ( ITelegramUpdateHandler handler , Update update )
132196 {
197+ CheckDisposed ( ) ;
133198 bool hasUser = update . TryGetUser ( out User user ) ;
134199 if ( ! hasUser )
135200 {
@@ -162,5 +227,13 @@ private async Task HandleRequestAsync(ITelegramUpdateHandler handler, Update upd
162227 throw new InvalidOperationException ( "Invalid result type: " + result . GetType ( ) . Name ) ;
163228 }
164229 }
230+
231+ private void CheckDisposed ( )
232+ {
233+ if ( _disposed )
234+ {
235+ throw new ObjectDisposedException ( nameof ( BotApp ) ) ;
236+ }
237+ }
165238 }
166239}
0 commit comments