Skip to main content

Result Types

If every connector used its own error model — one throwing exceptions, another returning tuples, a third using custom error objects — the application code that calls connectors would be a mess of adapters and conditionals. The framework standardizes on a single result type for every operation: OperationResult<T>.

The result type carries three possible outcomes:

  • Success — the operation completed, and .Value contains the result
  • Validation failure — the input did not pass schema validation
  • Failure — the operation failed, and .Error provides a machine-readable code and a human-readable message

This tri-state model avoids the ambiguity of throwing exceptions for validation errors (which are not exceptional — they indicate caller bugs) and distinguishes transient provider failures from preventable input errors.

Every connector operation returns an OperationResult<T>. This gives you a consistent pattern for handling success, failure, and validation errors across all channels.

OperationResult<T>

OperationResult<T> is the standard return type for all connector operations. It comes from the Deveel.Results package.

Properties

PropertyTypeDescription
IsSuccess()boolOperation completed successfully (method)
IsFailure()boolOperation failed (method)
ValueT?Result value (non-null when IsSuccess)
ErrorIMessagingError?Error code and message (non-null when IsFailure)

Note: OperationResult<T> is provided by the Deveel.Results package. Use .IsSuccess() and .IsFailure() as methods.

Usage patterns

var result = await connector.SendMessageAsync(message, ct);

if (result.IsSuccess())
{
var sendResult = result.Value!;
Console.WriteLine($"Sent: {sendResult.RemoteMessageId}");
}
else if (result.IsFailure() && result.Error is IValidationError)
{
var validationError = (IValidationError)result.Error;
Console.WriteLine("Validation errors:");
foreach (var error in validationError.Errors)
Console.WriteLine($" - {error.ErrorMessage}");
}
else
{
Console.WriteLine($"Error [{result.Error!.Code}]: {result.Error.Message}");
}

Factory methods

// Success
return OperationResult<SendResult>.Success(new SendResult { ... });

// Failure with error code and message
return OperationResult<SendResult>.Fail("RATE_LIMITED", "SMS", "Too many requests");

// Validation failure
return OperationResult<SendResult>.ValidationFailed(
"VALIDATION_ERROR", "SMS", validationResults);

// Implicit conversion from T
SendResult sendResult = await SendToProviderAsync(message);
return sendResult; // auto-wraps in OperationResult<SendResult>

The implicit conversion is what makes ChannelConnectorBase overrides clean — your core methods return raw values, and the base class wraps them.

SendResult

Returned by IChannelConnector.SendMessageAsync.

var result = await connector.SendMessageAsync(message, ct);

if (result.IsSuccess())
{
Console.WriteLine($"Local ID: {result.Value!.MessageId}");
Console.WriteLine($"Provider ID: {result.Value.RemoteMessageId}");
Console.WriteLine($"Initial status: {result.Value.Status}");
Console.WriteLine($"Timestamp: {result.Value.Timestamp}");
}
PropertyTypeDescription
MessageIdstringThe local message ID you assigned
RemoteMessageIdstringProvider-assigned message identifier
StatusMessageStatus?Initial delivery status from provider
TimestampDateTimeOffset?When the provider accepted the message
AdditionalDataIDictionary<string, object>Provider-specific metadata; includes RetryAttempts when retry policy is active

The GetRetryAttempts() extension method reads the retry count from AdditionalData:

var result = await connector.SendMessageAsync(message, ct);
Console.WriteLine($"Attempts: {result.Value.GetRetryAttempts()}"); // 1 if no retries

In a connector override

protected override async Task<SendResult> SendMessageCoreAsync(
IMessage message, CancellationToken ct)
{
var apiResult = await _httpClient.PostAsync("/send", ...);

return new SendResult
{
MessageId = message.Id,
RemoteMessageId = apiResult.Id,
Status = MessageStatus.Sent,
Timestamp = DateTimeOffset.UtcNow,
AdditionalData = new Dictionary<string, object>
{
["provider_fee"] = apiResult.Fee,
["remaining_balance"] = apiResult.Balance
}
};
}

BatchSendResult

Returned by IChannelConnector.SendBatchAsync.

var batch = new MessageBatch();
batch.Messages.Add(msg1);
batch.Messages.Add(msg2);

var result = await connector.SendBatchAsync(batch, ct);

if (result.IsSuccess())
{
var data = result.Value!;
Console.WriteLine($"Batch {data.BatchId}: {data.MessageResults.Count} messages");

foreach (var (msgId, sendResult) in data.MessageResults)
{
if (sendResult.Status == MessageStatus.Sent)
Console.WriteLine($" {msgId}: sent ({sendResult.RemoteMessageId})");
else
Console.WriteLine($" {msgId}: {sendResult.Status}");
}
}
PropertyTypeDescription
BatchIdstringLocal batch identifier
RemoteBatchIdstring?Provider-assigned batch ID (if supported)
MessageResultsIDictionary<string, SendResult>Per-message results keyed by message ID

ReceiveResult

Returned by IChannelConnector.ReceiveMessagesAsync.

var source = new MessageSource("application/json", webhookBody);
var result = await connector.ReceiveMessagesAsync(source, ct);

if (result.IsSuccess())
{
var data = result.Value!;
Console.WriteLine($"Received {data.Messages.Count} message(s)");

foreach (var message in data.Messages)
{
Console.WriteLine($" From: {message.Sender?.Address}");
Console.WriteLine($" Content: {(message.Content as TextContent)?.Text}");
}
}
PropertyTypeDescription
BatchIdstringIdentifier for this receive batch
MessagesIReadOnlyList<IMessage>Received messages

MessageSource

The MessageSource struct represents an inbound message payload (from webhooks, callbacks, etc.):

public readonly struct MessageSource
{
public string ContentType { get; } // e.g., "application/json"
public string? ContentEncoding { get; } // optional encoding
public ReadOnlyMemory<byte> Content { get; } // raw payload

// Static factories for common formats
MessageSource.Json(...)
MessageSource.Xml(...)
MessageSource.Text(...)
MessageSource.Binary(...)
MessageSource.UrlPost(...)

// Parsing helpers
ReadOnlySpan<char> AsText();
T AsJson<T>();
}

StatusUpdateResult / StatusUpdatesResult

Returned by IChannelConnector.ReceiveMessageStatusAsync and GetMessageStatusAsync.

// Get message delivery history
var statusResult = await connector.GetMessageStatusAsync("msg-1", ct);

if (statusResult.IsSuccess())
{
foreach (var update in statusResult.Value!.Updates)
{
Console.WriteLine(
$"[{update.Timestamp:O}] {update.Status}" +
$"{(update.Description != null ? $": {update.Description}" : "")}");
}
}

StatusUpdateResult properties:

PropertyTypeDescription
MessageIdstringThe message ID
StatusMessageStatusStatus value (Received, Queued, Sent, Delivered, DeliveryFailed, etc.)
TimestampDateTimeOffsetWhen the status was recorded
Descriptionstring?Optional human-readable description
AdditionalDataIDictionary<string, object>Provider-specific metadata; includes RetryAttempts when retry policy is active

StatusUpdatesResult properties:

PropertyTypeDescription
MessageIdstringThe message ID
UpdatesIList<StatusUpdateResult>Ordered list of status updates

StatusInfo

Returned by IChannelConnector.GetStatusAsync.

var status = await connector.GetStatusAsync(ct);

if (status.IsSuccess())
{
var info = status.Value!;
Console.WriteLine($"Connector status: {info.Status}");

if (info.Description != null)
Console.WriteLine($"Details: {info.Description}");

Console.WriteLine($"Last updated: {info.Timestamp:O}");
}
PropertyTypeDescription
StatusstringStatus string (provider-specific)
Descriptionstring?Optional description
TimestampDateTimeOffsetWhen the status was determined
AdditionalDataIDictionary<string, object>Provider-specific metadata; includes RetryAttempts when retry policy is active

ConnectorHealth

Returned by IChannelConnector.GetHealthAsync.

var health = await connector.GetHealthAsync(ct);

if (health.IsSuccess())
{
var h = health.Value!;
Console.WriteLine($"Healthy: {h.IsHealthy}");
Console.WriteLine($"State: {h.State}");
Console.WriteLine($"Uptime: {h.Uptime}");
Console.WriteLine($"Last check: {h.LastHealthCheck:O}");

if (h.Issues.Count > 0)
{
Console.WriteLine("Issues:");
foreach (var issue in h.Issues)
Console.WriteLine($" - {issue}");
}

if (h.Metrics.Count > 0)
{
Console.WriteLine("Metrics:");
foreach (var (key, value) in h.Metrics)
Console.WriteLine($" {key}: {value}");
}
}
PropertyTypeDescription
StateConnectorStateCurrent lifecycle state
IsHealthybooltrue if the connector is operating normally
LastHealthCheckDateTimeWhen the health check was last run
UptimeTimeSpanTime since last successful initialization
MetricsDictionary<string, object>Custom metrics (message count, error rate, latency, etc.)
IssuesList<string>Human-readable issue descriptions

IMessagingError

public interface IMessagingError
{
string Code { get; }
string? Message { get; }
}

Error handling

The framework uses a three-layer error handling model:

  1. Return valuesOperationResult<T> is the standard return type for all connector operations
  2. ExceptionsConnectorException and MessagingException are thrown for non-recoverable errors during initialization, connection testing, and send operations (caught by the base class and converted to OperationResult<T>)
  3. Error codes — Every error has a machine-readable string code and a human-readable message

ConnectorException

Thrown inside connector operations to signal a recoverable or provider-specific error. The base class catches it and wraps it in OperationResult<T>.Fail():

throw new ConnectorException(
MessagingErrorCodes.InvalidRecipient,
TwilioErrorCodes.ErrorDomain,
"Recipient phone number is required");

Error code hierarchy

Error codes are organized by scope in static classes:

ClassDomainScope
MessagingErrorCodes"messaging"General messaging errors
ConnectorErrorCodes"messaging"Connector lifecycle and operation errors
FacebookErrorCodes"Facebook"Facebook Messenger API errors
FirebaseErrorCodes"Firebase"Firebase Cloud Messaging errors
TelegramErrorCodes"Telegram"Telegram Bot API errors
TwilioErrorCodes"Twilio"Twilio SMS/WhatsApp errors
SendGridErrorCodes"SendGrid"SendGrid email errors

ConnectorException

Thrown inside connector lifecycle methods (InitializeAsync, TestConnectionAsync, SendMessageCoreAsync, ReceiveMessagesCoreAsync, etc.) to signal a specific error. Contains an error code, a domain string, and a human-readable message.

Authentication errors

Authentication providers (AuthenticationProviderBase subclasses) return AuthenticationResult with a string error code when authentication fails. The AuthenticationManager wraps these into a ConnectorException with code AUTHENTICATION_FAILED or reports the provider-specific code directly.

Error code tables

MessagingErrorCodes (general)

These codes are used for messaging-level errors outside the scope of channel connectors, such as routing and configuration.

CodeDescription
MESSAGING_ERRORUnspecified or unexpected messaging error
MESSAGE_ROUTING_FAILEDMessage could not be routed to the intended recipient or channel
MESSAGE_SERIALIZATION_FAILEDMessage serialization failed
MESSAGE_DESERIALIZATION_FAILEDMessage deserialization failed
INVALID_CONFIGURATIONConnector configuration is invalid or incomplete
UNSUPPORTED_CONTENT_TYPEUnsupported message content type encountered
CONNECTOR_NOT_FOUNDNo connector was found for the requested channel type
INVALID_WEBHOOK_DATAWebhook data is invalid or malformed
INVALID_RECIPIENTRecipient endpoint is missing, invalid, or unreachable
MISSING_CREDENTIALSRequired credentials are missing
INVALID_CREDENTIALSProvided credentials are invalid or expired
MISSING_SENDERSender endpoint is missing or not configured
MESSAGE_TOO_LONGMessage exceeds the maximum allowed length
CONNECTION_FAILEDConnection to the remote service failed
SEND_MESSAGE_FAILEDSending a message failed
RATE_LIMIT_EXCEEDEDAPI rate limit has been exceeded

ConnectorErrorCodes (connector operations)

These codes are defined in Ratatosk.Connectors and used by ChannelConnectorBase.

CodeDescription
ALREADY_INITIALIZEDConnector has already been initialized
INITIALIZATION_ERRORError occurred during connector initialization
AUTHENTICATION_FAILEDAuthentication with the remote service failed
CONNECTION_TEST_ERRORError testing connection to the external service
MESSAGE_VALIDATION_FAILEDMessage validation failed before sending
SEND_MESSAGE_ERRORError sending a single message
BATCH_VALIDATION_FAILEDBatch validation failed before sending
SEND_BATCH_ERRORError sending a batch of messages
GET_STATUS_ERRORError retrieving connector status
GET_MESSAGE_STATUS_ERRORError retrieving message status
GET_HEALTH_ERRORError performing health check
RECEIVE_STATUS_ERRORError receiving status updates
RECEIVE_MESSAGES_ERRORError receiving messages

Authentication error codes

These codes are returned by authentication providers when obtaining or refreshing credentials fails.

CodeDescription
MISSING_API_KEYAPI key not found in connection settings
MISSING_TOKENBearer token not found
MISSING_BASIC_CREDENTIALSBasic authentication credentials (username/password) not found
MISSING_PARAMETERSRequired OAuth parameters (client ID, secret) are missing
MISSING_TOKEN_ENDPOINTToken endpoint URL is required but missing
TOKEN_REQUEST_FAILEDToken request to the provider failed
INVALID_TOKEN_RESPONSEToken response is missing the access token
EMPTY_ACCESS_TOKENEmpty access token received from provider
INVALID_REFRESH_RESPONSERefresh response is missing the access token
EMPTY_REFRESH_TOKENEmpty access token received from refresh
REFRESH_FAILEDToken refresh operation failed
NETWORK_ERRORNetwork error during token request
TIMEOUTToken request timed out
INVALID_JSONInvalid JSON in provider response
UNEXPECTED_ERRORUnexpected error during authentication
MISSING_SERVICE_ACCOUNT_KEYService account key is required but missing
SERVICE_ACCOUNT_FILE_NOT_FOUNDService account key file does not exist
INVALID_SERVICE_ACCOUNT_JSONService account key is not valid JSON
CREDENTIAL_ERRORError preparing credential
NO_PROVIDERNo authentication provider available for the requested scheme
AUTHENTICATION_ERRORUnspecified authentication error

Error code mapping

Channel connectors map provider-specific API errors to the framework's error codes through dedicated mapping methods in their service implementations:

  • Twilio: TwilioService.MapTwilioErrorCode() maps Twilio ApiException.Code integers
  • Telegram: TelegramService.MapTelegramErrorCode() and MapTelegramSendErrorCode() map Telegram ApiRequestException.ErrorCode integers
  • Firebase: FirebaseService.MapFirebaseErrorCode() maps Firebase MessagingErrorCode enum values
  • Facebook: Error codes are assigned directly via ConnectorException, without provider error code translation
  • SendGrid: HTTP status codes are mapped in the connector; no custom error code mapping

See the channel-specific documentation for detailed mapping tables.