So since my last post on Slack's new Socket Mode I've been working with it a little more and getting it ready to be used in a cloud environment. At first I was just taking a slightly more refined version of the existing code I posted and place it inside a website.

This was until I had a conversation with Ankur Oberoi on the Slack developer tools team and he made a suggestion about how to improve the way users could interact with Socket Mode. He was right and this article is going to explain the suggestion he made and then how to implement the resulting NuGet package into an ASP.Net Core website.

The reason I'm focusing on a website is because frankly I'd rather let Kestrel worrying about wiring up and running an isolated background process for me and then just upload that result into Azure or AWS or wherever you want. The WebSocket connection you make is from your outgoing connection so the fact it's a website doesn't matter.

So - what was the suggestion?

The suggestion was that rather than having to get your users to handle a loop of messages with a WebSocket connection - it made more sense to try and change the flow of information so that it came through an IAsyncEnumerable.

If you've not come across one before, it was released in .NET Core 3 and C# 8.0 and what it allows you to do is take an async method and yield from it so that the consumers of your method can use it within a loop, using a special await foreach syntax. Because you're using yield that means that your actual method can be more complicated than a simple collection but that's abstracted away for those using your code.

From a Slack Socket Mode perspective this was great news! It meant that I could write a client that managed the connection, the hello and disconnect messages, but that the primary output from the client was an IAsyncEnumerable<Envelope>, making it a clear and clean loop of objects that the user was receiving from Slack.

await foreach (var envelope in client.EnvelopeAsyncEnumerable(token))
{
    if (envelope.Payload is SlashCommand) //for example
    {
      //your logic here
    }
}

So that's exactly what I did and released the first version of Slack.NetStandard.AsyncEnumerable and it's this client that we're going to put into our website.

File->New->Website

So in this example I'm literally going from File->New because here the website is just the housing for the application. The only thing I've changed from the defaults that appear on my screen is that I've enabled Docker support purely because I know that at some point in the future I'm going to want to deploy this to the cloud and Docker makes that easier for me.

The file new screen of Visual Studio after selecting a web application

I've picked the empty template as it saves me time removing all the unwanted parts. It gives me a single endpoint at root which I can use as a status check to ensure that the app is in fact running in case of any problems.

What we're going to be focusing on is the way in which the .NET Core website can run background services, and one such service is going to be our Socket Mode client. For that we need to install the NuGet package Slack.NetStandard.AsyncEnumerable and then we're off.

Socket Mode Background Service

Okay. So we create a new class in the website app named SocketModeService.cs and we give this a base class of BackgroundService which is a handy helper class from Microsoft. The default implementation requires only one method - ExecuteAsync. But we're going to use two overridable methods too, StartAsync and StopAsync.

There's one property and that's the Slack.NetStandard client we installed.

using Microsoft.Extensions.Hosting;

public class SocketModeService:BackgroundService
{
      private SocketModeClient Client { get; }

      public override Task StartAsync(CancellationToken cancellationToken)
      {
          return base.StartAsync(cancellationToken);
      }

      protected override Task ExecuteAsync(CancellationToken stoppingToken)
      {
          throw new NotImplementedException();
      }

      public override Task StopAsync(CancellationToken cancellationToken)
      {
          return base.StopAsync(cancellationToken);
      }
}

The idea is that when your app starts each registered service will spin up. The service will have its StartAsync method executed and then run the ExecuteAsync method.

That method is expected to run as long as required - indefinitely if need be - and when the application is shutting down the service will be notified that the method has to stop by triggering the cancellation on the CancellationToken that has been passed in. Once the method has finished then StopAsync is called (if you don't tidy up your service in time it will be forcably shut down so don't worry - a service can't hold the entire app hostage indefinitely)

This infrastructure is great for us as it manages the lifecycle so we can focus on the logic.

For my example I'm going to place the new client inside the class, but it should probably be handled via its own dependency injection inside the website startup class.

So we start with...well...Start

public override async Task StartAsync(CancellationToken cancellationToken)
{
    Client = new SocketModeClient();
    await Client.ConnectAsync(appToken, cancellationToken);
}

So here we're creating the new client and getting it connected. The connect method needs access to the Web API and does that with the appToken (available from the "Basic Info" screen of your Slack app) or it can take a Slack.NetStandard SlackWebApiClient if you've already got one available. The client itself can take an existing WebSocketClient if you need to use one that's been customised.

The reason for the cancellationToken is so that if the app does shut down even while the service is spinning up we're able to be as good a part of the system as possible - no point in continuing waiting for a connection if we're shutting down.

So now we should have an active client. The next thing to be sure of is that we shut down correctly, so we'll alter StopAsync

public override async Task StopAsync(CancellationToken cancellationToken)
{
    if (Client.WebSocket.State == WebSocketState.Open)
    {
        await Client.WebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "App shutting down", cancellationToken);
    }
}

So if when the service is finished the underlying connection is still open, we just close it correctly. I did think about writing the client to handle this - but for now with the WebSocket possibly having been passed in I felt there were enough instances to leave it alone. I may change that behavior to be configurable in a future version - but right now a few extra lines and it's sorted.

So now for the big bit - the actual execution! The receiving of the Socket Mode envelopes.

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    await foreach (var envelope in Client.EnvelopeAsyncEnumerable(stoppingToken))
    {
        //Logic goes here
    }
}

Bit of an anti-climax? Good! What we want is simple reliable code and this is just going to keep looping objects as they arrive.

I guess it would be nice if we showed an example of an implementation though? Well all my projects use the Slack.NetStandard.RequestHandler package to help isolate the Slack logic from the host app, and there's a handy extension method that links RequestHandlers to Envelope objects in Slack.NetStandard.Endpoint

So my initial implementation looks like this. But feel free to put an if or switch statement in there, whatever you want.

  private SlackPipeline<Message> _pipeline = CreateMyPipeline();

  protected override async Task ExecuteAsync(CancellationToken stoppingToken)
  {
      await foreach (var envelope in Client.EnvelopeAsyncEnumerable(stoppingToken))
      {
          var response = await _pipeline.Process(envelope.ToSlackInformation(), envelope);
          var ack = new Acknowledge{EnvelopeId=envelope.EnvelopeId, Payload = response}
          await Client.Send(JsonConvert.SerializeObject(ack), stoppingToken);
      }
  }

So this runs my logic for each request, and then the response type from the SlackPipeline is a message (or null if no message is required).

Regardless of whether or not there's a message needing to be sent back, you must send back an Acknowledgement object back to Slack within a few seconds with the appropriate EnvelopeId otherwise the Slack user will be told there was an error, and for that we use the client's Send Method.

Now none of this is going to work yet - because we've created the service, but we've not actually registered it to start up. So go back to your Startup class and make sure that ConfigureServices has this line in it

public void ConfigureServices(IServiceCollection services)
{
    services.AddHostedService<SocketModeService>();
}

And there we go - your service can now spin up and handle things at it's heart content.....

...well....maybe. Probably.

One service is good. Is two better?

We are, I promise, done with implementing a Slack Socket app in terms of communicating with Slack. A few lines in each of the service methods and we're done. If you're happy then feel free to stop reading now.

The issue now is that you've implemented this loop to receive messages from all your users, possibly dozens of workspaces, but each message is being read in turn, actioned, and then the next one in the loop is being picked up.

You have to respond to the original message with an acknowledgement in 3 seconds. But what happens to your users if one message takes 5 seconds on its own? All the pending messages are expiring before you've had time to get to them.

So what do we do?

We seperate the reading of the messages and the processing of the messages. We can acknowledge every message immediately, and then pass the envelope to another service which handles the logic. That logic, somewhere else, can use the message information to update the user in a few seconds time.

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    await foreach (var envelope in Client.EnvelopeAsyncEnumerable(stoppingToken))
    {
        var ack = new Acknowledge {EnvelopeId = envelope.EnvelopeId};
        await Client.Send(JsonConvert.SerializeObject(ack), stoppingToken);
        await PassEnvelope(envelope, stoppingToken);
    }
}

public async Task PassEnvelope(Envelope envelope, CancellationToken token)
{
  //movement logic here.
}

The rest of the article is going to give you an implementation you can use to handle this PassEnvelope method to get you started - at this point you have an object that can be converted to and from JSON so you can handle it any way you want to.

So the way I deal with this - because I want to keep everything inside this one web app, is that I use Channels.

Channels

If you've not come across Channels before in .NET Core then here's an introduction from Steve Gordon. Essentially a Channel is a thread-safe in-memory queue - a way of passing data from a producer to a consumer in an async friendly way.

So the way this starts is that I add a new typed Channel to my DI in Startup.cs

services.AddSingleton(Channel.CreateUnbounded<Envelope>());

As you'll read in the article Unbounded means there's no limit to how many messages it will queue, whether that's right for you is your choice - but a bounded queue means that you may start to get throughput errors with slack of you can't read the messages off the queue fast enough.

Once I've got my channel I want to add it to my SocketModeService constructor and add it to a property

private Channel<Envelope> _channel;

public SocketModeService(Channel<Envelope> channel)
{
    _channel = channel;
}

And we can then use the channel to will in the implementation of the PassEnvelope method

public async Task PassEnvelope(Envelope envelope, CancellationToken token)
{
    await _channel.Writer.WriteAsync(envelope, token);
}

So now we have this queue of envelopes being written to as fast as that service can handle it. So where do they go? Well we create another service that reads those same messages off that same channel

public class EnvelopeReaderService : BackgroundService
{
    private readonly Channel<Envelope> _channel;

    public EnvelopeReaderService(Channel<Envelope> channel)
    {
        _channel = channel;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await _channel.Reader.WaitToReadAsync(stoppingToken);
            if (_channel.Reader.TryRead(out var envelope))
            {
                //Slack logic here
            }
        }
    }
}

and you register this service alongside your other one:

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton(Channel.CreateUnbounded<Envelope>());
    services.AddHostedService<EnvelopeReaderService>();
    services.AddHostedService<SocketModeService>();
}

And there you have it

Now you've got a single website app that can handle Slack Socket Mode communication as well as slack processing logic at the same time.

It's a naive implementation, certainly, but hopefully it gives you enough to get started and try it out for yourself. Once that initial acknowledgement is handled, where that data goes is up to you but you've received it without having to allow incoming connections to a public API and that's the big win for Socket Mode.

Have fun!