Overview
The Result class represents handler execution results and allows you to communicate with the router and control aspect execution. It determines whether a handler succeeded, failed, or whether the router should continue searching for other handlers.
Properties
Indicates if the result is positive (success).
Indicates whether the router should search for the next matching handler.
The exact type that the router should search for when chaining handlers.
Static Methods
Result.Ok()
public static Result Ok()
Represents a successful execution.
Behavior in different contexts:
- Inside
IPreProcessor: Allows the handler’s main execution block to run.
- Inside handler’s
Execute() method: Tells the router that the update was handled successfully and it can stop searching for other handlers.
- Inside
FiltersFallback(): Allows the router to continue searching for other handlers.
Returns: A positive result that stops router execution.
Example:
public override async Task<Result> Execute(
IHandlerContainer<Message> container,
CancellationToken cancellation)
{
await Reply("Command executed successfully!");
return Result.Ok(); // Success, stop looking for other handlers
}
Result.Fault()
public static Result Fault()
Represents a fault or error state.
Behavior in different contexts:
- Inside
IPreProcessor: Interrupts execution of the handler. The main block and IPostProcessor will not be executed.
- Inside
FiltersFallback(): Interrupts the router’s handler search sequence.
- Inside handler’s
Execute() method: Indicates an error occurred and stops router execution.
Returns: A negative result that stops execution.
Example:
public override async Task<Result> Execute(
IHandlerContainer<Message> container,
CancellationToken cancellation)
{
if (!await ValidateUser())
{
await Reply("Access denied!");
return Result.Fault(); // Error, stop processing
}
await ProcessCommand();
return Result.Ok();
}
Result.Next()
public static Result Next()
Represents a continuation signal.
Behavior in different contexts:
- Inside
FiltersFallback(): Allows the router to continue searching for other handlers.
- Inside handler’s
Execute() method: Tells the router to continue searching for and executing other matching handlers.
Returns: A positive result that continues router execution.
Example:
[AnyUpdateHandler]
public class LoggingHandler : AnyUpdateHandler
{
public override async Task<Result> Execute(
IHandlerContainer<Update> container,
CancellationToken cancellation)
{
// Log the update
_logger.LogInformation("Update received: {UpdateId}", Input.Id);
// Continue to next handler
return Result.Next();
}
}
Result.Next<T>()
public static Result Next<T>()
Represents a chaining continuation with type filtering.
Behavior:
- Inside handler’s
Execute() method: Tells the router to continue searching for handlers, but only execute handlers of the exact type T.
Type Parameters:
The handler type to search for next.
Returns: A positive result that continues to specific handler types.
Example:
[MessageHandler]
public class PreprocessHandler : MessageHandler
{
public override async Task<Result> Execute(
IHandlerContainer<Message> container,
CancellationToken cancellation)
{
// Store some data for other handlers
ExtraData["preprocessed"] = true;
ExtraData["timestamp"] = DateTime.UtcNow;
// Continue only to CommandHandler instances
return Result.Next<CommandHandler>();
}
}
Decision Flow
Here’s how to choose which result to return:
Did the handler succeed?
├─ Yes
│ └─ Should other handlers run?
│ ├─ No → Result.Ok()
│ ├─ Yes, any handler → Result.Next()
│ └─ Yes, specific type → Result.Next<T>()
└─ No (error occurred)
└─ Result.Fault()
Examples
Success and Stop
[CommandHandler]
[CommandFilter("start")]
public class StartHandler : CommandHandler
{
public override async Task<Result> Execute(
IHandlerContainer<Message> container,
CancellationToken cancellation)
{
await Reply("Welcome to the bot!");
// This handler completely handled the update
return Result.Ok();
}
}
Error Handling
[CommandHandler]
[CommandFilter("admin")]
public class AdminHandler : CommandHandler
{
public override async Task<Result> Execute(
IHandlerContainer<Message> container,
CancellationToken cancellation)
{
var user = Input.From;
if (!await _adminService.IsAdmin(user.Id))
{
await Reply("⛔ You don't have admin privileges.");
return Result.Fault(); // Stop processing due to auth failure
}
await ProcessAdminCommand();
return Result.Ok();
}
}
Logging and Continue
[AnyUpdateHandler(importance: 100)]
public class AuditHandler : AnyUpdateHandler
{
public override async Task<Result> Execute(
IHandlerContainer<Update> container,
CancellationToken cancellation)
{
// Log to audit trail
await _auditLog.RecordUpdate(Input);
// Continue to other handlers
return Result.Next();
}
}
Handler Chain
[MessageHandler(importance: 10)]
public class ValidationHandler : MessageHandler
{
public override async Task<Result> Execute(
IHandlerContainer<Message> container,
CancellationToken cancellation)
{
// Validate the message
if (Input.Text?.Length > 1000)
{
await Reply("Message too long!");
return Result.Fault(); // Stop processing
}
// Store validation result
ExtraData["validated"] = true;
// Continue to other MessageHandler instances
return Result.Next<MessageHandler>();
}
}
[MessageHandler(importance: 5)]
public class ProcessingHandler : MessageHandler
{
public override async Task<Result> Execute(
IHandlerContainer<Message> container,
CancellationToken cancellation)
{
// This only runs if validation passed
if (ExtraData.ContainsKey("validated"))
{
await ProcessMessage();
}
return Result.Ok();
}
}
Conditional Routing
[MessageHandler]
public class RouterHandler : MessageHandler
{
public override async Task<Result> Execute(
IHandlerContainer<Message> container,
CancellationToken cancellation)
{
if (Input.Text?.StartsWith("/") == true)
{
// Let command handlers process this
return Result.Next<CommandHandler>();
}
if (Input.Photo != null)
{
// Let photo handlers process this
return Result.Next<PhotoHandler>();
}
// No specific handler needed, continue normally
return Result.Next();
}
}
Middleware Pattern
[AnyUpdateHandler(importance: 50)]
public class RateLimitMiddleware : AnyUpdateHandler
{
public override async Task<Result> Execute(
IHandlerContainer<Update> container,
CancellationToken cancellation)
{
var userId = GetUserId(Input);
if (await _rateLimiter.IsRateLimited(userId))
{
// Rate limited - stop processing
return Result.Fault();
}
// Not rate limited - continue to next handler
return Result.Next();
}
}
Use Cases by Result Type
When to use Result.Ok()
- Command was processed successfully
- Response was sent to user
- No other handlers should run
- Final handler in a chain
When to use Result.Fault()
- Authentication/authorization failed
- Validation errors occurred
- Rate limit exceeded
- Critical error that should stop processing
When to use Result.Next()
- Logging/analytics handlers
- Preprocessing/middleware
- Non-terminal operations
- Want all matching handlers to run
When to use Result.Next<T>()
- Building handler pipelines
- Routing to specific handler types
- Implementing workflows with specific handler sequences
- Type-safe handler chaining
Best Practices
-
Use Ok() for terminal handlers: When your handler fully processes the update and no other handlers should run.
-
Use Next() for middleware: When implementing cross-cutting concerns like logging, analytics, or validation.
-
Use Fault() sparingly: Reserve for actual errors, not for normal flow control.
-
Chain with Next<T>(): Build type-safe handler pipelines for complex workflows.
-
Document behavior: Comment why you’re using each result type, especially
Next<T>().
See Also