Overview
The Result class is how handlers communicate with the router. Results control whether routing continues, stops, or branches, giving you fine-grained control over update processing.
The Result Class
public sealed class Result
{
/// < summary >
/// Is result positive
/// </ summary >
public bool Positive { get ; }
/// < summary >
/// Should router search for next matching handler
/// </ summary >
public bool RouteNext { get ; }
/// < summary >
/// Exact type that router should search
/// </ summary >
public Type ? NextType { get ; }
}
Result Types
Telegrator provides four primary result types:
Result.Ok() Success - stop routing and mark as handled
Result.Next() Continue - let other handlers process too
Result.Fault() Error - stop routing due to failure
Result.Next<T>() Chain - continue only with specific type
Result.Ok()
Indicates successful handler execution and stops the routing chain.
When to Use
Use Result.Ok() when:
The handler fully processed the update
No other handlers should execute
The operation completed successfully
Example
using Telegram . Bot . Types ;
using Telegrator . Handlers ;
using Telegrator . Annotations ;
[ CommandHandler ]
[ CommandAllias ( "start" )]
public class StartCommandHandler : CommandHandler
{
public override async Task < Result > Execute (
IHandlerContainer < Message > container ,
CancellationToken cancellation )
{
await Reply ( "Welcome to the bot!" );
// Update fully handled, stop routing
return Result . Ok ();
}
}
Behavior
Result.Ok() is the most common return value. Use it whenever your handler successfully processes the update and no further handling is needed.
Result.Next()
Indicates the handler processed the update but other handlers should also execute.
When to Use
Use Result.Next() when:
You want to log or track updates
Multiple handlers should process the same update
Implementing middleware-style behavior
The handler performed a partial operation
Example: Logging Handler
[ MessageHandler ]
[ HasText ]
public class LoggingHandler : MessageHandler
{
private readonly ILogger < LoggingHandler > _logger ;
public LoggingHandler ( ILogger < LoggingHandler > logger )
{
_logger = logger ;
}
public override async Task < Result > Execute (
IHandlerContainer < Message > container ,
CancellationToken cancellation )
{
// Log the message
_logger . LogInformation (
"User {UserId} sent: {Text}" ,
Input . From ? . Id ,
Input . Text );
// Let other handlers process the message
return Result . Next ();
}
}
[ MessageHandler ]
[ TextEquals ( "hello" )]
public class HelloHandler : MessageHandler
{
public override async Task < Result > Execute (
IHandlerContainer < Message > container ,
CancellationToken cancellation )
{
// This will execute after LoggingHandler
await Reply ( "Hello!" );
return Result . Ok ();
}
}
Behavior
Use Result.Next() for implementing cross-cutting concerns like logging, analytics, or rate limiting that shouldn’t interfere with normal handler execution.
Result.Fault()
Indicates an error occurred and routing should stop.
When to Use
Use Result.Fault() when:
An error occurred that can’t be recovered
The update shouldn’t be processed further
You want to signal failure to the router
Validation failed critically
Example: Validation Handler
[ MessageHandler ]
[ CommandHandler ]
[ CommandAllias ( "admin" )]
public class AdminCommandHandler : CommandHandler
{
private readonly IUserService _userService ;
public AdminCommandHandler ( IUserService userService )
{
_userService = userService ;
}
public override async Task < Result > Execute (
IHandlerContainer < Message > container ,
CancellationToken cancellation )
{
long userId = Input . From ? . Id ?? 0 ;
// Check if user is admin
if ( ! await _userService . IsAdminAsync ( userId ))
{
await Reply ( "⚠️ You don't have permission for this command." );
// Stop routing - unauthorized access
return Result . Fault ();
}
// User is admin, execute command
await Reply ( "⚙️ Admin panel loading..." );
return Result . Ok ();
}
}
Example: Error Handling
[ MessageHandler ]
[ TextStartsWith ( "/calculate" )]
public class CalculatorHandler : MessageHandler
{
public override async Task < Result > Execute (
IHandlerContainer < Message > container ,
CancellationToken cancellation )
{
try
{
// Parse and calculate
string expression = Input . Text . Substring ( 11 );
double result = Calculate ( expression );
await Reply ( $"Result: { result } " );
return Result . Ok ();
}
catch ( Exception ex )
{
await Reply ( $"❌ Error: { ex . Message } " );
// Signal error to router
return Result . Fault ();
}
}
private double Calculate ( string expression )
{
// Calculation logic...
throw new NotImplementedException ();
}
}
Behavior
Result.Fault() stops the routing chain. Use it carefully, as it prevents other handlers from processing the update.
Result.Next<T>()
Indicates routing should continue but only with handlers of a specific type.
When to Use
Use Result.Next<T>() when:
Implementing handler chains
Building pipeline processing
Creating handler hierarchies
You want type-specific routing
Example: Handler Chain
// Base processing handler
[ MessageHandler ]
[ TextStartsWith ( "/process" )]
public class ProcessStartHandler : MessageHandler
{
public override async Task < Result > Execute (
IHandlerContainer < Message > container ,
CancellationToken cancellation )
{
// Initial processing
await Reply ( "🔄 Processing started..." );
// Continue only with ProcessingStepHandler types
return Result . Next < ProcessingStepHandler >();
}
}
// Step 1
[ MessageHandler ]
public class ValidationStepHandler : ProcessingStepHandler
{
public override async Task < Result > Execute (
IHandlerContainer < Message > container ,
CancellationToken cancellation )
{
// Validation logic
await Reply ( "✅ Validation complete" );
// Continue with next ProcessingStepHandler
return Result . Next < ProcessingStepHandler >();
}
}
// Step 2
[ MessageHandler ]
public class TransformationStepHandler : ProcessingStepHandler
{
public override async Task < Result > Execute (
IHandlerContainer < Message > container ,
CancellationToken cancellation )
{
// Transformation logic
await Reply ( "🔄 Transformation complete" );
// Continue with next ProcessingStepHandler
return Result . Next < ProcessingStepHandler >();
}
}
// Final step
[ MessageHandler ]
public class FinalizationStepHandler : ProcessingStepHandler
{
public override async Task < Result > Execute (
IHandlerContainer < Message > container ,
CancellationToken cancellation )
{
// Finalization logic
await Reply ( "✅ Processing complete!" );
// Stop the chain
return Result . Ok ();
}
}
// Base class for processing steps
public abstract class ProcessingStepHandler : MessageHandler
{
}
Behavior
Result.Next<T>() is useful for creating processing pipelines where you want to execute multiple related handlers in sequence.
Results in Different Contexts
In Handler Execute Method
public override async Task < Result > Execute (
IHandlerContainer < Message > container ,
CancellationToken cancellation )
{
// Result.Ok() - Handler succeeded, stop routing
// Result.Next() - Handler succeeded, continue routing
// Result.Fault() - Handler failed, stop routing
// Result.Next<T>() - Continue with type T handlers only
return Result . Ok ();
}
In FiltersFallback
When filters fail, you can control routing in the fallback:
public override async Task < Result > FiltersFallback (
FiltersFallbackReport report ,
ITelegramBotClient client ,
CancellationToken cancellationToken = default )
{
// Result.Next() - Silently continue to next handler
// Result.Fault() - Stop routing (reject update)
// Send error message and continue
await client . SendTextMessageAsync (
chatId : report . Context . Update . Message . Chat . Id ,
text : "Filter validation failed" ,
cancellationToken : cancellationToken );
return Result . Next (); // Let other handlers try
}
In Aspects (Pre/Post Processors)
public class LoggingPreProcessor : IPreProcessor
{
public async Task < Result > PreProcess (
UpdateHandlerBase handler ,
IHandlerContainer container ,
CancellationToken cancellationToken )
{
// Log before execution
// Result.Ok() - Let handler execute
// Result.Fault() - Prevent handler execution
return Result . Ok ();
}
}
Decision Tree
Use this tree to decide which result to return:
Common Patterns
Pattern 1: Simple Command
Most commands return Result.Ok():
[ CommandHandler ]
[ CommandAllias ( "help" )]
public class HelpHandler : CommandHandler
{
public override async Task < Result > Execute (
IHandlerContainer < Message > container ,
CancellationToken cancellation )
{
await Reply ( "Help message..." );
return Result . Ok (); // Done, no other handlers needed
}
}
Pattern 2: Middleware/Logging
Logging handlers return Result.Next():
[ MessageHandler ]
public class AnalyticsHandler : MessageHandler
{
public override async Task < Result > Execute (
IHandlerContainer < Message > container ,
CancellationToken cancellation )
{
// Track analytics
await TrackEvent ( "message_received" , Input . From ? . Id );
return Result . Next (); // Let other handlers process
}
}
Pattern 3: Validation
Validation handlers return Result.Fault() on failure:
[ MessageHandler ]
public class RateLimitHandler : MessageHandler
{
public override async Task < Result > Execute (
IHandlerContainer < Message > container ,
CancellationToken cancellation )
{
if ( await IsRateLimited ( Input . From ? . Id ))
{
await Reply ( "⏱️ Too many requests. Please wait." );
return Result . Fault (); // Block further processing
}
return Result . Next (); // Pass to next handler
}
}
Pattern 4: Pipeline Processing
Pipeline handlers use Result.Next<T>():
[ MessageHandler ]
public class PipelineStarter : MessageHandler
{
public override async Task < Result > Execute (
IHandlerContainer < Message > container ,
CancellationToken cancellation )
{
// Start pipeline
return Result . Next < PipelineStep >();
}
}
Best Practices
Choose the result that clearly expresses what should happen: // Good: Clear intent
await Reply ( "Done!" );
return Result . Ok (); // We're done
// Bad: Unclear intent
await Reply ( "Done!" );
return Result . Next (); // Why continue if we're done?
Handle Errors Appropriately
Use Result.Fault() for errors that should stop processing: try
{
await PerformOperation ();
return Result . Ok ();
}
catch ( Exception ex )
{
_logger . LogError ( ex , "Operation failed" );
await Reply ( "An error occurred" );
return Result . Fault (); // Don't let other handlers try
}
Use Next() for Cross-Cutting Concerns
Logging, analytics, and monitoring should use Result.Next(): // Logging handler
_logger . LogInformation ( "Processing update" );
return Result . Next (); // Don't block other handlers
// Analytics handler
await TrackEvent ( "user_action" );
return Result . Next (); // Let normal handlers run
When using Result.Next<T>(), document the chain: /// < summary >
/// Order processing pipeline:
/// 1. OrderValidationHandler
/// 2. OrderPricingHandler
/// 3. OrderConfirmationHandler
/// </ summary >
[ MessageHandler ]
public class OrderPipelineStarter : MessageHandler
{
public override async Task < Result > Execute (...)
{
return Result . Next < OrderPipelineHandler >();
}
}
Result Matrix
Result Positive RouteNext NextType Use Case Ok()✅ True ❌ False null Handler succeeded, stop routing Next()✅ True ✅ True null Handler succeeded, continue routing Fault()❌ False ❌ False null Handler failed, stop routing Next<T>()✅ True ✅ True Type T Handler succeeded, route to type T
Handlers Learn about implementing handlers
Mediator Pattern Understand the routing mechanism
Filters Master filter validation and fallbacks
Architecture See how results fit in the framework