Skip to main content

Quickstart

Every interaction with the framework follows the same lifecycle regardless of which channel provider you use:

  1. Register — add the Ratatosk package and configure connectors in the DI container
  2. Build — construct an IMessage using MessageBuilder
  3. Send/Receive — call IMessagingClient.SendAsync() or IMessagingClient.ReceiveAsync()
  4. Handle — check .IsSuccess(), read the result value, or inspect .Error

This guide walks through the IMessagingClient approach — the recommended pattern for all applications. Connectors are infrastructure: you register them in DI and never instantiate them directly.

1. Create a project

dotnet new console -o MessagingDemo
cd MessagingDemo
dotnet add package Ratatosk
dotnet add package Ratatosk.Twilio

2. Send an SMS

Register the connector in DI, inject IMessagingClient, build a message, and send it:

// Program.cs
using Ratatosk;

var builder = WebApplication.CreateBuilder(args);

builder.Services
.AddMessaging()
.AddConnector<TwilioSmsConnector>("sms", cfg => cfg
.WithSettings("Twilio"))
.AddClient();

builder.Services.AddSingleton<NotificationService>();

var app = builder.Build();
app.Run();

// NotificationService.cs
public class NotificationService(IMessagingClient messagingClient)
{
public async Task<string?> SendSmsAsync(string to, string text)
{
var message = new MessageBuilder()
.WithId(Guid.NewGuid().ToString("n"))
.FromPhone("+15550001111")
.ToPhone(to)
.WithText(text)
.Build();

var result = await messagingClient.SendAsync("sms", message);

if (result.IsSuccess)
return result.Value?.RemoteMessageId;

throw new InvalidOperationException(
$"SMS failed: {result.Error?.Message}");
}
}

The client handles connector resolution, lazy initialization, caching, and disposal. You never interact with IChannelConnector directly.

3. Same code, different channel

Add another connector and send through it using the same client — only the channel name and endpoint types change:

builder.Services
.AddMessaging()
.AddConnector<TwilioSmsConnector>("sms", cfg => cfg
.WithSettings("Twilio"))
.AddConnector<SendGridEmailConnector>("email", cfg => cfg
.WithSettings("SendGrid"))
.AddClient();

// In your service:
var smsResult = await client.SendAsync("sms", smsMessage);
var emailResult = await client.SendAsync("email", emailMessage);

The MessageBuilder, the IMessagingClient methods, and the OperationResult<T> pattern are identical across channels.

4. Receive inbound messages (webhook)

Channels like Twilio, Telegram, and Facebook push inbound messages via webhooks. The client's ReceiveAsync normalises the payload into IMessage objects:

[HttpPost("/webhooks/twilio")]
public async Task<IActionResult> TwilioWebhook(CancellationToken ct)
{
using var reader = new StreamReader(Request.Body);
var body = await reader.ReadToEndAsync(ct);
var source = MessageSource.UrlPost(body);

var result = await _client.ReceiveAsync("sms", source, ct);

if (result.IsSuccess)
{
foreach (var message in result.Value?.Messages ?? [])
Console.WriteLine($"Received: {message.Id} from {message.Sender}");
return Ok();
}

return BadRequest(result.Error?.ErrorMessage);
}

Status callbacks (delivery receipts, read receipts) are handled through ReceiveMessageStatusAsync on the client, with the same MessageSource pattern.

5. Advanced resolution strategies

The IMessagingClient supports three resolution strategies that can be mixed in the same application.

Resolution by name (named connectors)

Register multiple channels with distinct names and send through each by name:

builder.Services
.AddMessaging()
.AddConnector<TwilioSmsConnector>("sms-primary", cfg => cfg
.WithSettings("Twilio:Primary"))
.AddConnector<TwilioSmsConnector>("sms-fallback", cfg => cfg
.WithSettings("Twilio:Fallback"))
.AddConnector<SendGridEmailConnector>("email", cfg => cfg
.WithSettings("SendGrid"))
.AddClient();

// Usage
var result = await client.SendAsync("sms-primary", message);
if (result.IsFailure)
result = await client.SendAsync("sms-fallback", message);

Resolution by type (anonymous connectors)

When you register a single unnamed connector, resolve it through the generic overload:

builder.Services
.AddMessaging()
.AddConnector<TwilioSmsConnector>(cfg => cfg.WithSettings("Twilio"))
.AddClient();

var result = await client.SendAsync<TwilioSmsConnector>(message);

Runtime resolution

For applications where connection settings are loaded at runtime (from a database, API, or external configuration), register the connector type at startup without providing settings, and supply the settings at the call site:

// Program.cs — register the type, no settings
builder.Services
.AddMessaging()
.AddConnectorType<FacebookMessengerConnector>("facebook")
.AddClient();

// In a request handler — load settings at runtime
var runtimeSettings = new ConnectionSettings()
.SetParameter("PageAccessToken", accessToken)
.SetParameter("PageId", pageId);

var result = await client.SendAsync("facebook", runtimeSettings, message);

The same pattern works with type parameters for anonymous runtime resolution:

builder.Services
.AddMessaging()
.AddConnectorType<FacebookMessengerConnector>()
.AddClient();

await client.SendAsync<FacebookMessengerConnector>(runtimeSettings, message);

Auto-initialization

The client automatically initializes the connector on first use. To disable this (for scenarios where initialization is handled externally), configure MessagingClientOptions:

builder.AddClient(o => o.AutoInitialize = false);

Next steps