Skip to content

The search box in the website knows all the secrets—try it!

For any queries, join our Discord Channel to reach us faster.

JasperFx Logo

JasperFx provides formal support for Wolverine and other JasperFx libraries. Please check our Support Plans for more details.

Using Amazon SNS

WARNING

At this moment, Wolverine cannot support request/reply mechanics (IMessageBus.InvokeAsync<T>()) with SNS.

TIP

Due to the nature of SNS, Wolverine doesn't include any listening functionality for this transport. You may forward messages to Amazon SQS and use it in conjunction with the SQS transport to listen for incoming messages.

Wolverine supports Amazon SNS as a messaging transport through the WolverineFx.AmazonSns package.

Connecting to the Broker

First, if you are using the shared AWS config and credentials files, the SNS connection is just this:

cs
var host = await Host.CreateDefaultBuilder()
    .UseWolverine(opts =>
    {
        // This does depend on the server having an AWS credentials file
        // See https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html for more information
        opts.UseAmazonSnsTransport()

            // Let Wolverine create missing topics and subscriptions as necessary
            .AutoProvision();
    }).StartAsync();

snippet source | anchor

cs
var builder = Host.CreateApplicationBuilder();
builder.UseWolverine(opts =>
{
    var config = builder.Configuration;

    opts.UseAmazonSnsTransport(snsConfig =>
        {
            snsConfig.ServiceURL = config["AwsUrl"];
            // And any other elements of the SNS AmazonSimpleNotificationServiceConfig
            // that you may need to configure
        })

        // Let Wolverine create missing topics and subscriptions as necessary
        .AutoProvision();
});

using var host = builder.Build();
await host.StartAsync();

snippet source | anchor

If you'd just like to connect to Amazon SNS running from within LocalStack on your development box, there's this helper:

cs
var host = await Host.CreateDefaultBuilder()
    .UseWolverine(opts =>
    {
        // Connect to an SNS broker running locally
        // through LocalStack
        opts.UseAmazonSnsTransportLocally();
    }).StartAsync();

snippet source | anchor

And lastly, if you want to explicitly supply an access and secret key for your credentials to SNS, you can use this syntax:

cs
var builder = Host.CreateApplicationBuilder();
builder.UseWolverine(opts =>
{
    var config = builder.Configuration;

    opts.UseAmazonSnsTransport(snsConfig =>
        {
            snsConfig.ServiceURL = config["AwsUrl"];
            // And any other elements of the SNS AmazonSimpleNotificationServiceConfig
            // that you may need to configure
        })

        // And you can also add explicit AWS credentials
        .Credentials(new BasicAWSCredentials(config["AwsAccessKey"], config["AwsSecretKey"]))

        // Let Wolverine create missing topics and subscriptions as necessary
        .AutoProvision();
});

using var host = builder.Build();
await host.StartAsync();

snippet source | anchor

Publishing

Configuring subscriptions through Amazon SNS topics is done with the ToSnsTopic() extension method shown in the example below:

cs
var host = await Host.CreateDefaultBuilder()
    .UseWolverine(opts =>
    {
        opts.UseAmazonSnsTransport();

        opts.PublishMessage<Message1>()
            .ToSnsTopic("outbound1")

            // Increase the outgoing message throughput, but at the cost
            // of strict ordering
            .MessageBatchMaxDegreeOfParallelism(Environment.ProcessorCount)
            .ConfigureTopicCreation(conf =>
            {
                // Configure topic creation request...
            });
    }).StartAsync();

snippet source | anchor

Connecting to Multiple Brokers 6.17

If you need to connect to more than one Amazon SNS broker (a different AWS account, region, or endpoint) from a single Wolverine application, register additional, named brokers alongside the default one and pin publishing rules to a specific broker by name:

cs
using var host = await Host.CreateDefaultBuilder()
    .UseWolverine(opts =>
    {
        // The "default" broker
        opts.UseAmazonSnsTransport();

        // Add a second, named Amazon SNS broker that connects to a
        // different account or region
        opts.AddNamedAmazonSnsBroker(new BrokerName("americas"), snsConfig =>
        {
            snsConfig.RegionEndpoint = RegionEndpoint.USEast1;
        }).AutoProvision();

        // Publish to a topic on the named broker. The Uri scheme for these
        // endpoints is the broker name, so you'd see "americas://colors".
        opts.PublishMessage<Message1>()
            .ToSnsTopicOnNamedBroker(new BrokerName("americas"), "colors");
    }).StartAsync();

snippet source | anchor

Note that the Uri scheme within Wolverine for any endpoints from a "named" Amazon SNS broker is the name that you supply for the broker. So in the example above, you would see Uri values like americas://colors.

TIP

Because SNS is publish-only, the receiving side of a named SNS broker is handled by an SQS queue subscribed to the topic. A named SNS broker and a same-named SQS broker cannot share a single BrokerName (Wolverine keys transports by their Uri scheme, and the SNS broker owns that name), so run the SQS listener that drains the subscribed queue on the default (or a differently-named) SQS broker. The SNS-side subscription provisioning uses its own paired SQS client that targets the same account/region as the named SNS broker.

Multi-Tenancy with a Broker per Tenant 6.17

Wolverine can give each tenant its own dedicated Amazon SNS connection (a distinct AWS account/credentials, region, or ServiceURL) while sharing one declared topic topology. Which connection a message is published to is decided at runtime by the message's tenant id, typically set through DeliveryOptions.TenantId:

cs
using var host = await Host.CreateDefaultBuilder()
    .UseWolverine(opts =>
    {
        // The "default" / shared SNS connection
        opts.UseAmazonSnsTransport(config =>
        {
            config.RegionEndpoint = RegionEndpoint.USEast1;
        })
            .AutoProvision()

            // How should Wolverine route a message whose TenantId is null or
            // unknown? FallbackToDefault (the default) uses the shared connection;
            // TenantIdRequired throws; IgnoreUnknownTenants silently drops it.
            .TenantIdBehavior(TenantedIdBehavior.FallbackToDefault)

            // Each tenant gets its OWN dedicated SNS connection, but shares the
            // topic topology declared below. This tenant inherits the parent's
            // AWS credentials, and just re-points at its own region.
            .AddTenant("tenant-west", config =>
            {
                config.RegionEndpoint = RegionEndpoint.USWest2;
            })

            // Or give the tenant its own dedicated AWS account by supplying
            // its own credentials (optionally with its own region/endpoint too):
            .AddTenant("tenant-eu", new BasicAWSCredentials("tenant-eu-key", "tenant-eu-secret"),
                config =>
                {
                    config.RegionEndpoint = RegionEndpoint.EUWest1;
                });

        // SNS is publish-only, so broker-per-tenant means tenant-specific PUBLISHERS.
        // A message is published to the right tenant's connection at runtime by its
        // Envelope.TenantId (e.g. new DeliveryOptions { TenantId = "tenant-west" }).
        opts.PublishMessage<Message1>().ToSnsTopic("colors");
    }).StartAsync();

snippet source | anchor

To route a specific message to a tenant's connection, stamp the tenant id on the send:

csharp
await bus.SendAsync(new ColorMessage("blue"), new DeliveryOptions { TenantId = "tenant-west" });

SNS is publish-only

SNS has no listening surface in Wolverine, so broker-per-tenant support here means tenant-specific publishers only — Wolverine wraps the outbound topic in a TenantedSender that dispatches on Envelope.TenantId to a per-tenant SNS client. There is no per-tenant listener.

The receiving side is handled exactly as it is for a single-tenant SNS setup: by subscribing an SQS queue to the tenant's topic and listening on the SQS side. For full per-tenant consumption, pair this with Amazon SQS broker-per-tenant multi-tenancy so that each tenant's subscribed queue is consumed on that tenant's own SQS connection. Each SNS tenant already provisions its topic subscription and queue policy through its own paired SQS client, so the SNS and SQS tenant connections line up on the same account/region.

TenantIdBehavior(...) controls what happens when a message has a null or unregistered tenant id:

  • FallbackToDefault (the default) — publish it to the shared/default connection (the one passed to UseAmazonSnsTransport).
  • TenantIdRequired — throw; every message must carry a known tenant id.
  • IgnoreUnknownTenants — silently drop the message.

Named broker vs. broker-per-tenant

Use a named broker when a fixed set of endpoints should always publish to a specific broker. Use broker-per-tenant when the same logical topic should be transparently routed to a different connection per tenant based on the runtime tenant id. They are independent features and can be combined.

Topic Subscriptions

Wolverine gives you the ability to automatically subscribe SQS Queues to SNS topics with it's auto-provision feature through the SubscribeSqsQueue() extension method.

cs
var host = await Host.CreateDefaultBuilder()
    .UseWolverine(opts =>
    {
        opts.UseAmazonSnsTransport()
            // Without this, the SubscribeSqsQueue() call does nothing
            // This tells Wolverine to try to ensure all topics, subscriptions,
            // and SQS queues exist at runtime 
            .AutoProvision()
            
            // *IF* you need to use some kind of custom queue policy in your
            // SQS queues *and* want to use AutoProvision() as well, this is 
            // the hook to customize that policy. This is the default though that
            // we're just showing for an example
            .QueuePolicyForSqsSubscriptions(description =>
            {
                return $$"""
                         {
                           "Version": "2012-10-17",
                           "Statement": [{
                               "Effect": "Allow",
                               "Principal": {
                                   "Service": "sns.amazonaws.com"
                               },
                               "Action": "sqs:SendMessage",
                               "Resource": "{{description.QueueArn}}",
                               "Condition": {
                                 "ArnEquals": {
                                     "aws:SourceArn": "{{description.TopicArn}}"
                                 }
                               }
                           }]
                         }
                         """;
            });

        opts.PublishMessage<Message1>()
            .ToSnsTopic("outbound1")
            // Sets a subscriptions to be
            .SubscribeSqsQueue("queueName",
                config =>
                {
                    // Configure subscription attributes
                    config.RawMessageDelivery = true;
                });
    }).StartAsync();

snippet source | anchor

Interoperability

TIP

Also see the more generic Wolverine Guide on Interoperability

SNS interoperability is done through the ISnsEnvelopeMapper. At this point, SNS supports interoperability through MassTransit, NServiceBus, CloudEvents, or user defined mapping strategies.

URI reference

The SnsEndpointUri helper class builds canonical endpoint URIs:

URI formHelper call
sns://{name}SnsEndpointUri.Topic("name")
csharp
using Wolverine.AmazonSns;

var uri = SnsEndpointUri.Topic("events");
// FIFO topic (suffix preserved verbatim):
var fifoUri = SnsEndpointUri.Topic("events.fifo");

Released under the MIT License.