Skip to content

Commit 7927b81

Browse files
committed
Implement IHost in BotApp and enhance lifecycle management
BotApp.cs: - Implemented IHost interface in BotApp class. - Added private field _disposed to track disposal state. - Added CancellationTokenSource field _cancellationTokenSource. - Updated constructor to accept a CancellationTokenSource parameter. - Updated Run method to use a merged cancellation token. - Added StartAsync, StopAsync, Dispose, and MergeTokens methods. - Updated ErrorHandler, UpdateHandler, and HandleRequestAsync to call CheckDisposed. - Added private method CheckDisposed to throw ObjectDisposedException. BotBuilder.cs: - Updated BotBuilder to create and pass a CancellationTokenSource to BotApp. HostApplicationLifetime.cs: - Updated to accept a CancellationTokenSource through its constructor. - Removed creation of its own CancellationTokenSource.
1 parent 4b86f9e commit 7927b81

3 files changed

Lines changed: 100 additions & 22 deletions

File tree

Sources/TelegramBot/BotApp.cs

Lines changed: 88 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,29 +13,39 @@
1313
using Microsoft.Extensions.Logging;
1414
using Microsoft.Extensions.DependencyInjection;
1515
using Microsoft.Extensions.Hosting;
16+
using Newtonsoft.Json.Linq;
1617

1718
namespace 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&lt;HostBuilderContext,IServiceCollection&gt;)" />).
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
}

Sources/TelegramBot/Builders/BotBuilder.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Microsoft.Extensions.Hosting;
99
using Microsoft.Extensions.Configuration;
1010
using Microsoft.Extensions.DependencyInjection;
11+
using System.Threading;
1112

1213
namespace TelegramBot.Builders
1314
{
@@ -136,8 +137,9 @@ public IBot Build()
136137
{
137138
Services.AddSingleton<IKeyValueProvider, InMemoryKeyValueProvider>();
138139
}
139-
Services.AddSingleton<IHostApplicationLifetime, HostApplicationLifetime>();
140-
return new BotApp(client, Services.BuildServiceProvider());
140+
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
141+
Services.AddSingleton<IHostApplicationLifetime, HostApplicationLifetime>(x => new HostApplicationLifetime(cancellationTokenSource));
142+
return new BotApp(client, Services.BuildServiceProvider(), cancellationTokenSource);
141143
}
142144
}
143145
}

Sources/TelegramBot/Services/HostApplicationLifetime.cs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
1-
using System;
2-
using System.Threading;
1+
using System.Threading;
32
using Microsoft.Extensions.Hosting;
43

54
namespace TelegramBot.Services
65
{
76
internal class HostApplicationLifetime : IHostApplicationLifetime
87
{
8+
private readonly CancellationTokenSource _cancellationTokenSource;
9+
10+
public HostApplicationLifetime(CancellationTokenSource cancellationTokenSource)
11+
{
12+
_cancellationTokenSource = cancellationTokenSource ?? throw new System.ArgumentNullException(nameof(cancellationTokenSource));
13+
}
14+
915
public CancellationToken ApplicationStarted => _cancellationTokenSource.Token;
1016

1117
public CancellationToken ApplicationStopping => _cancellationTokenSource.Token;
1218

1319
public CancellationToken ApplicationStopped => _cancellationTokenSource.Token;
1420

15-
16-
private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
17-
1821
public void StopApplication()
1922
{
2023
_cancellationTokenSource.Cancel();

0 commit comments

Comments
 (0)