Framework Overview
A single programming model for SMS, email, push, and chat. The framework provides the contracts and base infrastructure; you provide the business logic.
Mental model
┌──────────────┐ ┌───────────────────┐ ┌──────────────────┐
│ IMessage │ ──▶ │ IChannelConnector │ ──▶ │ OperationResult │
│ (fluent │ │ (send, receive, │ │ (success or │
│ builder) │ │ status, health) │ │ typed error) │
└──────────────┘ └───────────────────┘ └──────────────────┘
- Build an
IMessageusing the fluentMessagebuilder — set sender, receiver, content, and properties - Send or receive through an
IChannelConnector— the same interface for SMS, email, push, and chat - Handle the
OperationResult<T>— check.IsSuccess, read.Value, or inspect.Error/.ValidationErrors
These three steps are identical regardless of which provider connector you use. Swapping from Twilio SMS to SendGrid email means changing the connector type and endpoint types — the send flow stays the same.
Example: one message shape, two channels
// Build the message once
var message = new MessageBuilder()
.WithId("welcome-1")
.FromEmail("team@example.com")
.ToEmail("newuser@example.com")
.WithText("Welcome to the platform!")
.WithSubject("Welcome!")
.Build();
// Send via SendGrid
var emailResult = await sendGridConnector.SendMessageAsync(message, ct);
// Or send the same content via Twilio SMS (just change endpoint type)
var smsMessage = new MessageBuilder()
.WithId("welcome-1")
.FromPhone("+15550001111")
.ToPhone("+15550002222")
.WithText("Welcome to the platform!")
.Build();
var smsResult = await twilioConnector.SendMessageAsync(message, ct);
Core building blocks
Message and endpoints
Message / IMessage — the central unit of work. Carries an ID, sender endpoint, receiver endpoint, content, and optional properties. Construct messages using MessageBuilder for a fluent API, or use new Message { ... } object initializers for explicit construction.
Endpoint / IEndpoint — a typed address. Instead of passing raw strings, every address is tagged with its type: PhoneNumber, EmailAddress, UserId, DeviceId, Url, Topic, ApplicationId, Id, Label, or Any. This enables schema validation to reject an email address where a phone number is expected before any provider API call.
Content
MessageContent / IMessageContent — abstract base for 12 content types:
| Type | Use case |
|---|---|
TextContent | Plain text (SMS, chat, fallback) |
HtmlContent | Rich HTML (email) |
MediaContent | Images, audio, video, documents |
BinaryContent | Raw binary data |
JsonContent | Structured JSON payloads |
LocationContent | Geo coordinates (chat) |
TemplateContent | Provider-side template rendering |
MultipartContent | Combination of multiple parts |
ButtonContent | Single interactive button (URL, postback, or phone number) |
QuickReplyContent | One-tap quick reply option above the keyboard |
CarouselContent | Horizontally scrollable set of cards with buttons |
ListPickerContent | Vertically scrollable list of selectable items |
The last four types implement the IInteractiveContent marker interface, which extends IMessageContent. Connectors that do not support interactive elements can check for this interface to reject unsupported content types early, during schema validation.
Schema
IChannelSchema — the contract that defines what a connector can do and what it needs to operate. Every connector has exactly one schema that describes:
- Its capabilities (send, receive, status query, bulk, templates, health checks, etc.)
- Required and optional connection parameters (API keys, endpoints, timeouts)
- Which endpoint types it handles and in which direction (send, receive, or both)
- Which content types it supports
- What message properties it understands and their validation rules
- What authentication methods it supports
ChannelSchema — the built-in fluent implementation of IChannelSchema. You construct it inline or via a schema factory.
Connectors
IChannelConnector — the primary contract for messaging operations:
InitializeAsync/ShutdownAsync— lifecycle control of the connector instanceSendMessageAsync/SendBatchAsync— outbound messaging operationsReceiveMessageStatusAsync— outbound status change signalingGetMessageStatusAsync— pulls the delivery state from the providerReceiveMessagesAsync— inbound messaging operationTestConnectionAsync/GetHealthAsync— diagnostic operations of the connectorValidateMessageAsync— per-message validation
ChannelConnectorBase — abstract base class that implements IChannelConnector. Provides state management, capability validation, message validation, authentication integration, structured logging scopes, and standardized error wrapping. To build a connector you override 4 abstract methods and optionally 7 virtual methods.
Messaging client
IMessagingClient — a high-level facade that resolves connectors from DI (by name or by type), handles lazy initialization, and exposes the 4 most common operations (SendAsync, ReceiveAsync, GetStatusAsync, ReceiveMessageStatusAsync). Implements IDisposable and IAsyncDisposable — when the client is disposed, all cached connectors are shut down gracefully. Callers do not manage connector state directly. Register it via .AddClient() on the MessagingBuilder.
The client supports three resolution strategies:
- By name —
SendAsync("channel-name", message)resolves pre-configured named connectors viaIChannelConnectorResolver(backed by DI keyed services) - By type —
SendAsync<TConnector>(message)resolves anonymous (unnamed) connectors from DI by type - At runtime with settings —
SendAsync("name", settings, message)orSendAsync<TConnector>(settings, message)creates connectors on-the-fly from runtime-providedConnectionSettings, using schema discovery andActivatorUtilities.CreateInstance
Connector type catalog
ConnectorTypeCatalog — a singleton registry that maps channel names to connector types (without connection settings). Populated at startup via AddConnectorType<TConnector>(). Used by the client's runtime overloads to know which connector type to instantiate for a given channel name.
Connector resolver
IChannelConnectorResolver — an abstraction for resolving pre-configured connectors by name. The default implementation (ServiceProviderConnectorResolver) delegates to DI keyed services. The client uses this internally for name-based resolution of statically registered connectors.
Message builder
MessageBuilder — an alternative fluent builder that separates construction from the model. Use it when the Message class's self-builder pattern is not desired:
var message = new MessageBuilder()
.WithId("msg-1")
.FromPhone("+15551234567")
.ToEmail("user@example.com")
.WithText("Hello!")
.WithSubject("Greeting")
.Build();
Results
OperationResult<T> — every operation returns this. It carries:
.IsSuccess/.IsFailure/.IsValidationFailure— status checks.Value— the result value on success.Error—IMessagingErrorwithErrorCodeandErrorMessageon failure.ValidationErrors— collection ofValidationResulton validation failure
Registry
IChannelSchemaRegistry — provides read-only access to all registered connector schemas. Use it to discover schemas at runtime (e.g., to drive a UI or validate a derived schema).
Package responsibilities
| Package | Role | Dependencies |
|---|---|---|
Ratatosk.Abstractions | Message model, MessageBuilder, endpoints, content types (including interactive). Pure model — no infrastructure. | None |
Ratatosk | AddMessaging() DI entry point, MessagingBuilder, IMessagingClient facade (disposable), ChannelConnectorFactory. | Connector.Abstractions |
Ratatosk.Connector.Abstractions | Interfaces for connectors, schemas, auth, and result types. Contracts only. | Abstractions |
Ratatosk.Connectors | ChannelConnectorBase, ChannelSchema builder, ChannelSchemaRegistry, authentication manager, ChannelConnectorBuilder. | Connector.Abstractions |
Ratatosk.Connector.* | Provider-specific implementations. Each references Connectors or Connector.Abstractions. | Provider SDK + Connectors |
Ratatosk.Senders | Sender identity infrastructure: ISenderRepository<TSender>, ISenderResolver, SenderManager<TSender>, distributed cache, and MessageBuilder.FromSender(). | Abstractions |
Ratatosk.Senders.InMemory | In-memory sender repository for development and testing, with optional seed data. | Ratatosk.Senders |
Ratatosk.Senders.EntityFramework | EF Core persistence for the sender registry (SenderDbContext, EntitySenderRepository, DbSender). | Ratatosk.Senders + EF Core |
What the framework does not do
The framework focuses on the messaging contract and connector consistency. It intentionally does not include:
- Queueing — messages are sent immediately; use a background queue (e.g., RabbitMQ, Azure Service Bus) for async delivery
- Scheduling — no built-in delayed delivery; combine with a scheduler library
- Persistence — messages are in-memory; persist to your database for auditing and retries
- Retries — transient failures are surfaced as errors; implement retry policies (Polly) at the application layer
- Templating engines — template rendering is delegated to providers (SendGrid, Twilio, etc.)
- Business workflows — approval chains, fallback routing, and orchestration are your application's responsibility
Design decisions
- MessageBuilder — the fluent
MessageBuilderclass (new MessageBuilder().WithX().WithY().Build()) separates construction from the model, and supports connector-specific extension methods (e.g.,WithTitle(),WithParseMode()). EachWith*()returns the builder for chaining. Explicitnew Message { ... }syntax is also supported for direct construction. - Raw values from overrides — when extending
ChannelConnectorBase, your core methods return raw values (likeSendResult), notOperationResult<T>. The base class wraps them, catches exceptions, and handles validation errors. This keeps connector code focused on API translation. - Schema-first — every connector has an
IChannelSchemathat drives validation, capability checks, and documentation. This makes connector behavior predictable and testable without calling provider APIs.