< Summary

Information
Line coverage
99%
Covered lines: 234
Uncovered lines: 1
Coverable lines: 235
Total lines: 591
Line coverage: 99.5%
Branch coverage
96%
Covered branches: 96
Total branches: 100
Branch coverage: 96%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/_/src/TeleFlow.Framework/Internal/Handlers/TelegramHandlerDispatcher.cs

#LineLine coverage
 1using Microsoft.Extensions.Logging;
 2using TeleFlow.Framework.Dispatching;
 3using TeleFlow.Framework.States;
 4using TeleFlow.Framework.Updates;
 5using TeleFlow.Telegram.Internal;
 6
 7namespace TeleFlow.Telegram.Internal.Handlers;
 8
 9/// <summary>
 10/// Selects and invokes the Telegram handler for a single update payload.
 11/// </summary>
 12internal sealed partial class TelegramHandlerDispatcher : IUpdateDispatcher
 13{
 14    private readonly TelegramHandlerSelector _selector;
 15    private readonly TelegramErrorHandlerIndex _errorHandlerIndex;
 16    private readonly TimeProvider _timeProvider;
 17    private readonly TelegramAutoAnswerCallbackDescriptor? _globalAutoAnswerCallback;
 18    private readonly ILogger<TelegramHandlerDispatcher> _logger;
 19
 20    /// <summary>
 21    /// Creates a dispatcher from registered handler descriptors and framework services.
 22    /// </summary>
 23    public TelegramHandlerDispatcher(
 24        IEnumerable<TelegramHandlerDescriptor> descriptors,
 25        IEnumerable<TelegramErrorHandlerDescriptor> errorHandlers,
 26        IEnumerable<TelegramAutoAnswerCallbackOptions> autoAnswerCallbackOptions,
 27        TelegramBotIdentity botIdentity,
 28        TimeProvider timeProvider,
 29        ILoggerFactory loggerFactory)
 30    {
 20231        ArgumentNullException.ThrowIfNull(descriptors);
 20232        ArgumentNullException.ThrowIfNull(errorHandlers);
 20233        ArgumentNullException.ThrowIfNull(autoAnswerCallbackOptions);
 20234        ArgumentNullException.ThrowIfNull(botIdentity);
 20235        ArgumentNullException.ThrowIfNull(timeProvider);
 20236        ArgumentNullException.ThrowIfNull(loggerFactory);
 37
 20238        var table = new TelegramHandlerTable(descriptors);
 39
 20140        _selector = new TelegramHandlerSelector(table, botIdentity, loggerFactory);
 20141        _errorHandlerIndex = new TelegramErrorHandlerIndex(errorHandlers);
 20142        _timeProvider = timeProvider;
 20143        _globalAutoAnswerCallback = CreateGlobalAutoAnswerDescriptor(autoAnswerCallbackOptions.LastOrDefault());
 20144        _logger = loggerFactory.CreateLogger<TelegramHandlerDispatcher>();
 20145    }
 46
 47    /// <summary>
 48    /// Dispatches one update through handler selection, invocation, error handlers, and callback auto-answering.
 49    /// </summary>
 50    public async Task DispatchAsync(UpdateContext context, CancellationToken cancellationToken = default)
 51    {
 34452        ArgumentNullException.ThrowIfNull(context);
 53
 34454        if (context.Payload is not TelegramUpdatePayload payload)
 55        {
 056            return;
 57        }
 58
 34459        var routeLoggingEnabled = _logger.IsEnabled(LogLevel.Information);
 34460        var debugEnabled = _logger.IsEnabled(LogLevel.Debug);
 34461        string? updateType = null;
 3162        string GetUpdateType() => updateType ??= TelegramUpdateLogFormatter.GetUpdateType(payload.Update);
 63
 34464        var effectiveCancellationToken = cancellationToken.CanBeCanceled
 34465            ? cancellationToken
 34466            : context.CancellationToken;
 34467        var currentState = _selector.HasStatefulHandlers
 34468            ? await GetCurrentStateAsync(context, effectiveCancellationToken).ConfigureAwait(false)
 34469            : null;
 34470        var matchStarted = debugEnabled ? _timeProvider.GetTimestamp() : 0;
 34471        TelegramRouteSelection? selection = null;
 34472        TelegramUpdateContext? telegramContext = null;
 73
 34474        if (payload.Update.Message is not null)
 75        {
 25876            var messageContext = context.GetMessageContext();
 25877            selection = await _selector.SelectMessageHandlerAsync(
 25878                messageContext,
 25879                currentState,
 25880                effectiveCancellationToken).ConfigureAwait(false);
 25681            telegramContext = messageContext;
 25682        }
 83
 34284        if (selection is null && payload.Update.CallbackQuery is not null)
 85        {
 7286            var callbackContext = context.GetCallbackQueryContext();
 7287            selection = await _selector.SelectCallbackHandlerAsync(
 7288                callbackContext,
 7289                currentState,
 7290                effectiveCancellationToken).ConfigureAwait(false);
 7091            telegramContext = callbackContext;
 7092        }
 93
 34094        if (selection is null && (payload.Update.ChatMember is not null || payload.Update.MyChatMember is not null))
 95        {
 1496            var chatMemberContext = context.GetChatMemberUpdatedContext();
 1497            var routeKind = payload.Update.ChatMember is not null
 1498                ? TelegramRouteKind.ChatMemberUpdated
 1499                : TelegramRouteKind.MyChatMemberUpdated;
 14100            selection = await _selector.SelectChatMemberHandlerAsync(
 14101                chatMemberContext,
 14102                routeKind,
 14103                currentState,
 14104                effectiveCancellationToken).ConfigureAwait(false);
 14105            telegramContext = chatMemberContext;
 14106        }
 107
 340108        var matchElapsedMilliseconds = debugEnabled ? GetElapsedMilliseconds(matchStarted) : 0;
 109
 340110        if (selection is null || telegramContext is null)
 111        {
 18112            if (debugEnabled)
 113            {
 1114                LogNoHandlerMatchedWithTiming(
 1115                    _logger,
 1116                    payload.Update.UpdateId,
 1117                    GetUpdateType(),
 1118                    matchElapsedMilliseconds);
 119            }
 17120            else if (routeLoggingEnabled)
 121            {
 1122                LogNoHandlerMatched(
 1123                    _logger,
 1124                    payload.Update.UpdateId,
 1125                    GetUpdateType());
 126            }
 127
 18128            return;
 129        }
 130
 322131        string? handlerName = null;
 322132        string? routeName = null;
 29133        string GetHandlerName() => handlerName ??= TelegramUpdateLogFormatter.FormatHandler(selection.Handler);
 29134        string GetRouteName() => routeName ??= TelegramUpdateLogFormatter.FormatRoute(selection.Route);
 135
 322136        if (debugEnabled)
 137        {
 10138            LogHandlerMatchedWithTiming(
 10139                _logger,
 10140                payload.Update.UpdateId,
 10141                GetUpdateType(),
 10142                GetHandlerName(),
 10143                GetRouteName(),
 10144                selection.Handler.ModuleName ?? string.Empty,
 10145                selection.Handler.SceneName ?? string.Empty,
 10146                matchElapsedMilliseconds);
 147        }
 312148        else if (routeLoggingEnabled)
 149        {
 6150            LogHandlerMatched(
 6151                _logger,
 6152                payload.Update.UpdateId,
 6153                GetUpdateType(),
 6154                GetHandlerName(),
 6155                GetRouteName(),
 6156                selection.Handler.ModuleName ?? string.Empty,
 6157                selection.Handler.SceneName ?? string.Empty);
 158        }
 159
 322160        var handlerTimingEnabled = debugEnabled;
 322161        var handlerStarted = handlerTimingEnabled ? _timeProvider.GetTimestamp() : 0;
 322162        using var requestTimingScope = handlerTimingEnabled
 322163            ? TelegramHandlerRequestTimingScope.Begin()
 322164            : null;
 165
 166        try
 167        {
 322168            await TelegramHandlerInvoker.InvokeAsync(
 322169                selection,
 322170                telegramContext,
 322171                effectiveCancellationToken).ConfigureAwait(false);
 172
 297173            await AutoAnswerCallbackAsync(
 297174                selection.Handler,
 297175                telegramContext,
 297176                effectiveCancellationToken).ConfigureAwait(false);
 295177        }
 27178        catch (Exception exception) when (!IsUpdateCancellation(exception, effectiveCancellationToken))
 179        {
 26180            RouteExecutionFailureLogContext? logContext = null;
 181            RouteExecutionFailureLogContext GetLogContext()
 182            {
 6183                logContext ??= new RouteExecutionFailureLogContext(
 6184                    payload.Update.UpdateId,
 6185                    GetUpdateType(),
 6186                    GetHandlerName(),
 6187                    GetRouteName(),
 6188                    selection.Handler.ModuleName ?? string.Empty,
 6189                    selection.Handler.SceneName ?? string.Empty,
 6190                    exception.GetType().FullName ?? exception.GetType().Name);
 191
 6192                return logContext.Value;
 193            }
 194
 26195            if (_logger.IsEnabled(LogLevel.Error))
 196            {
 5197                LogRouteExecutionFailure(
 5198                    exception,
 5199                    GetLogContext(),
 5200                    handlerTimingEnabled,
 5201                    handlerStarted,
 5202                    requestTimingScope);
 203            }
 204
 26205            if (_errorHandlerIndex.HasHandlers &&
 26206                await TryHandleErrorAsync(
 26207                    selection,
 26208                    telegramContext,
 26209                    exception,
 26210                    debugEnabled ? GetLogContext() : null,
 26211                    effectiveCancellationToken).ConfigureAwait(false))
 212            {
 18213                return;
 214            }
 215
 7216            throw;
 217        }
 218
 295219        if (!handlerTimingEnabled)
 220        {
 287221            return;
 222        }
 223
 8224        var completedHandlerElapsed = _timeProvider.GetElapsedTime(handlerStarted);
 8225        var completedTiming = requestTimingScope!.CreateSummary(_timeProvider, completedHandlerElapsed);
 226
 8227        LogRouteExecutionCompleted(
 8228            _logger,
 8229            payload.Update.UpdateId,
 8230            GetUpdateType(),
 8231            GetHandlerName(),
 8232            GetRouteName(),
 8233            completedHandlerElapsed.TotalMilliseconds,
 8234            completedTiming.RequestCount,
 8235            completedTiming.RequestElapsedMilliseconds,
 8236            completedTiming.HandlerLogicElapsedMilliseconds);
 331237    }
 238
 239    /// <summary>
 240    /// Reads current state only when the selector contains stateful handlers.
 241    /// </summary>
 242    private static async ValueTask<string?> GetCurrentStateAsync(
 243        UpdateContext context,
 244        CancellationToken cancellationToken)
 245    {
 81246        return context.TryGetState(out var state)
 81247            ? await state.GetAsync(cancellationToken).ConfigureAwait(false)
 81248            : null;
 81249    }
 250
 251    /// <summary>
 252    /// Returns elapsed milliseconds from a timestamp captured by the dispatcher time provider.
 253    /// </summary>
 254    private double GetElapsedMilliseconds(long startingTimestamp)
 255    {
 11256        return _timeProvider.GetElapsedTime(startingTimestamp).TotalMilliseconds;
 257    }
 258
 259    /// <summary>
 260    /// Sends the configured callback answer after a callback handler when the handler did not answer it explicitly.
 261    /// </summary>
 262    private async Task AutoAnswerCallbackAsync(
 263        TelegramHandlerDescriptor handler,
 264        TelegramUpdateContext context,
 265        CancellationToken cancellationToken)
 266    {
 297267        if (context is not CallbackQueryContext callbackContext ||
 297268            callbackContext.IsCallbackQueryAnswered)
 269        {
 234270            return;
 271        }
 272
 63273        var autoAnswer = handler.AutoAnswerCallback ?? _globalAutoAnswerCallback;
 274
 63275        if (autoAnswer is not { Enabled: true })
 276        {
 57277            return;
 278        }
 279
 6280        await callbackContext.Callback.AnswerAsync(
 6281            autoAnswer.Text,
 6282            autoAnswer.ShowAlert ? true : null,
 6283            cancellationToken).ConfigureAwait(false);
 295284    }
 285
 286    /// <summary>
 287    /// Tries registered Telegram error handlers in deterministic priority order.
 288    /// </summary>
 289    private async ValueTask<bool> TryHandleErrorAsync(
 290        TelegramRouteSelection selection,
 291        TelegramUpdateContext telegramContext,
 292        Exception exception,
 293        RouteExecutionFailureLogContext? logContext,
 294        CancellationToken cancellationToken)
 295    {
 21296        var errorContext = new TelegramErrorContext(
 21297            exception,
 21298            telegramContext,
 21299            selection.Handler.HandlerType,
 21300            selection.Handler.MethodName,
 21301            selection.Handler.ModuleName,
 21302            selection.Handler.SceneName,
 21303            selection.RouteValues);
 304
 67305        foreach (var errorHandler in _errorHandlerIndex.GetCandidates(selection, telegramContext, exception))
 306        {
 22307            var result = await TelegramErrorHandlerInvoker.InvokeAsync(
 22308                errorHandler,
 22309                errorContext,
 22310                telegramContext,
 22311                exception,
 22312                selection.RouteValues,
 22313                cancellationToken).ConfigureAwait(false);
 21314            var handled = result == TelegramErrorHandlingResult.Handled;
 315
 21316            if (logContext is { } context)
 317            {
 1318                LogErrorHandlerCompleted(
 1319                    _logger,
 1320                    context.UpdateId,
 1321                    context.UpdateType,
 1322                    context.Handler,
 1323                    context.Route,
 1324                    context.ModuleName,
 1325                    context.SceneName,
 1326                    context.ExceptionType,
 1327                    FormatErrorHandler(errorHandler),
 1328                    handled);
 329            }
 330
 21331            if (handled)
 332            {
 18333                return true;
 334            }
 3335        }
 336
 2337        return false;
 20338    }
 339
 340    /// <summary>
 341    /// Determines whether an exception represents cancellation requested for the current update.
 342    /// </summary>
 343    private static bool IsUpdateCancellation(Exception exception, CancellationToken cancellationToken)
 344    {
 27345        return exception is OperationCanceledException && cancellationToken.IsCancellationRequested;
 346    }
 347
 348    /// <summary>
 349    /// Formats an error handler name for diagnostic logs.
 350    /// </summary>
 351    private static string FormatErrorHandler(TelegramErrorHandlerDescriptor handler)
 352    {
 1353        return $"{handler.HandlerType.Name}.{handler.MethodName}";
 354    }
 355
 356    /// <summary>
 357    /// Converts global callback auto-answer options to the descriptor used by the dispatcher.
 358    /// </summary>
 359    private static TelegramAutoAnswerCallbackDescriptor? CreateGlobalAutoAnswerDescriptor(
 360        TelegramAutoAnswerCallbackOptions? options)
 361    {
 201362        return options is null
 201363            ? null
 201364            : new TelegramAutoAnswerCallbackDescriptor(options.Enabled, options.Text, options.ShowAlert);
 365    }
 366
 367}

/_/src/TeleFlow.Framework/Internal/Handlers/TelegramHandlerDispatcher.Logging.cs

#LineLine coverage
 1using Microsoft.Extensions.Logging;
 2
 3namespace TeleFlow.Telegram.Internal.Handlers;
 4
 5internal sealed partial class TelegramHandlerDispatcher
 6{
 7    /// <summary>
 8    /// Stable event ids emitted by the Telegram handler dispatcher.
 9    /// </summary>
 10    private static class LogEventIds
 11    {
 12        /// <summary>
 13        /// No route matched the incoming Telegram update.
 14        /// </summary>
 15        public const int NoHandlerMatched = 1;
 16
 17        /// <summary>
 18        /// A route matched the incoming Telegram update.
 19        /// </summary>
 20        public const int HandlerMatched = 2;
 21
 22        /// <summary>
 23        /// A selected Telegram route execution failed.
 24        /// </summary>
 25        public const int RouteExecutionFailed = 3;
 26
 27        /// <summary>
 28        /// A selected Telegram route execution completed successfully.
 29        /// </summary>
 30        public const int RouteExecutionCompleted = 4;
 31
 32        /// <summary>
 33        /// A Telegram error handler completed after a selected route execution failed.
 34        /// </summary>
 35        public const int ErrorHandlerCompleted = 5;
 36    }
 37
 38    /// <summary>
 39    /// Captures log fields shared by route-execution failure logs and error-handler completion logs.
 40    /// </summary>
 41    private readonly record struct RouteExecutionFailureLogContext(
 42        long UpdateId,
 43        string UpdateType,
 44        string Handler,
 45        string Route,
 46        string ModuleName,
 47        string SceneName,
 48        string ExceptionType);
 49
 50    /// <summary>
 51    /// Logs a route-execution failure with optional timing details collected only for debug diagnostics.
 52    /// </summary>
 53    private void LogRouteExecutionFailure(
 54        Exception exception,
 55        RouteExecutionFailureLogContext context,
 56        bool includeTiming,
 57        long handlerStarted,
 58        TelegramHandlerRequestTimingScope? requestTimingScope)
 59    {
 560        if (!includeTiming)
 61        {
 362            LogRouteExecutionFailed(
 363                _logger,
 364                exception,
 365                context.UpdateId,
 366                context.UpdateType,
 367                context.Handler,
 368                context.Route,
 369                context.ModuleName,
 370                context.SceneName,
 371                context.ExceptionType);
 372            return;
 73        }
 74
 275        ArgumentNullException.ThrowIfNull(requestTimingScope);
 76
 277        var handlerElapsed = _timeProvider.GetElapsedTime(handlerStarted);
 278        var timing = requestTimingScope.CreateSummary(_timeProvider, handlerElapsed);
 79
 280        LogRouteExecutionFailedWithTiming(
 281            _logger,
 282            exception,
 283            context.UpdateId,
 284            context.UpdateType,
 285            context.Handler,
 286            context.Route,
 287            context.ModuleName,
 288            context.SceneName,
 289            context.ExceptionType,
 290            handlerElapsed.TotalMilliseconds,
 291            timing.RequestCount,
 292            timing.RequestElapsedMilliseconds,
 293            timing.HandlerLogicElapsedMilliseconds);
 294    }
 95
 96    /// <summary>
 97    /// Logs that no Telegram handler matched an update.
 98    /// </summary>
 99    [LoggerMessage(
 100        EventId = LogEventIds.NoHandlerMatched,
 101        Level = LogLevel.Information,
 102        Message = "No Telegram handler matched. update_id={UpdateId}, type={UpdateType}.")]
 103    private static partial void LogNoHandlerMatched(
 104        ILogger logger,
 105        long updateId,
 106        string updateType);
 107
 108    [LoggerMessage(
 109        EventId = LogEventIds.NoHandlerMatched,
 110        Level = LogLevel.Debug,
 111        Message = "No Telegram handler matched. update_id={UpdateId}, type={UpdateType}, match_ms={MatchElapsedMilliseco
 112    private static partial void LogNoHandlerMatchedWithTiming(
 113        ILogger logger,
 114        long updateId,
 115        string updateType,
 116        double matchElapsedMilliseconds);
 117
 118    /// <summary>
 119    /// Logs the Telegram handler selected for an update.
 120    /// </summary>
 121    [LoggerMessage(
 122        EventId = LogEventIds.HandlerMatched,
 123        Level = LogLevel.Information,
 124        Message = "Telegram handler matched. update_id={UpdateId}, type={UpdateType}, handler={Handler}, route={Route}, 
 125    private static partial void LogHandlerMatched(
 126        ILogger logger,
 127        long updateId,
 128        string updateType,
 129        string handler,
 130        string route,
 131        string moduleName,
 132        string sceneName);
 133
 134    [LoggerMessage(
 135        EventId = LogEventIds.HandlerMatched,
 136        Level = LogLevel.Debug,
 137        Message = "Telegram handler matched. update_id={UpdateId}, type={UpdateType}, handler={Handler}, route={Route}, 
 138    private static partial void LogHandlerMatchedWithTiming(
 139        ILogger logger,
 140        long updateId,
 141        string updateType,
 142        string handler,
 143        string route,
 144        string moduleName,
 145        string sceneName,
 146        double matchElapsedMilliseconds);
 147
 148    /// <summary>
 149    /// Logs a Telegram route-execution failure without debug-only timing fields.
 150    /// </summary>
 151    [LoggerMessage(
 152        EventId = LogEventIds.RouteExecutionFailed,
 153        Level = LogLevel.Error,
 154        Message = "Telegram route execution failed. update_id={UpdateId}, type={UpdateType}, handler={Handler}, route={R
 155    private static partial void LogRouteExecutionFailed(
 156        ILogger logger,
 157        Exception exception,
 158        long updateId,
 159        string updateType,
 160        string handler,
 161        string route,
 162        string moduleName,
 163        string sceneName,
 164        string exceptionType);
 165
 166    /// <summary>
 167    /// Logs a Telegram route-execution failure with debug-only timing fields.
 168    /// </summary>
 169    [LoggerMessage(
 170        EventId = LogEventIds.RouteExecutionFailed,
 171        Level = LogLevel.Error,
 172        Message = "Telegram route execution failed. update_id={UpdateId}, type={UpdateType}, handler={Handler}, route={R
 173    private static partial void LogRouteExecutionFailedWithTiming(
 174        ILogger logger,
 175        Exception exception,
 176        long updateId,
 177        string updateType,
 178        string handler,
 179        string route,
 180        string moduleName,
 181        string sceneName,
 182        string exceptionType,
 183        double handlerElapsedMilliseconds,
 184        int telegramRequestCount,
 185        double telegramRequestElapsedMilliseconds,
 186        double handlerLogicElapsedMilliseconds);
 187
 188    /// <summary>
 189    /// Logs a successfully completed Telegram route execution with debug timing fields.
 190    /// </summary>
 191    [LoggerMessage(
 192        EventId = LogEventIds.RouteExecutionCompleted,
 193        Level = LogLevel.Debug,
 194        Message = "Telegram route execution completed. update_id={UpdateId}, type={UpdateType}, handler={Handler}, route
 195    private static partial void LogRouteExecutionCompleted(
 196        ILogger logger,
 197        long updateId,
 198        string updateType,
 199        string handler,
 200        string route,
 201        double handlerElapsedMilliseconds,
 202        int telegramRequestCount,
 203        double telegramRequestElapsedMilliseconds,
 204        double handlerLogicElapsedMilliseconds);
 205
 206    /// <summary>
 207    /// Logs the result returned by a Telegram error handler.
 208    /// </summary>
 209    [LoggerMessage(
 210        EventId = LogEventIds.ErrorHandlerCompleted,
 211        Level = LogLevel.Debug,
 212        Message = "Telegram error handler completed. update_id={UpdateId}, type={UpdateType}, handler={Handler}, route={
 213    private static partial void LogErrorHandlerCompleted(
 214        ILogger logger,
 215        long updateId,
 216        string updateType,
 217        string handler,
 218        string route,
 219        string moduleName,
 220        string sceneName,
 221        string exceptionType,
 222        string errorHandler,
 223        bool handled);
 224}