A lot of my recent development time has been around Slack apps and trying to make them easier to build for .NET developers. Primarily that was in the form of Slack.NetStandard as I felt there was a lot of functionality gaps in the existing ecosystem, especially around the Events API, and I'm really pleased with how that turned out.

So I had a need for a new Slack app with one of my recent ideas, and as I started to build the code I realised there's still a lot of code that had nothing to do with my idea - but just trying to get slack in a working position. And the reason for that was the way slack apps have to deal with endpoints.

So...the endpoints

Within any given slack app, the majority of your communication from Slack is going to arrive one of three ways:

Now each of these forms of communication have their own URL within your slack application, and when you look at the documentation each of them is sent ever so slightly differently too. What this means is that to wire up a slack application you've got several different ways of handling incoming requests. And so as you build your app and you want to make sure they feel like they flow properly - you tend to write several different endpoints, which when each has a slightly different set up actually means there's a long delay between you and your interactions.

The risk (other than a lot of infrastructure when you just want to do something in Slack) is that your logic is now separated as you try and handle different event types and merge them into a cohesive journey. Also - all your requests for all your endpoints have to be verified with your signing secret, so you have that to either duplicate or wire up a common path.

So as I was tired of this process I decided to resolve it by building a couple of new NuGet packages and then I wanted to see how fast I could wire them up with an Azure Function. I picked this not only because it was serverless, but also because I'm not as comfortable with Azure as I am AWS so I knew if it could happen quickly then it wouldn't be due to my familiarity.

Slack.NetStandard.Endpoint.XXX

So this is the first set of NuGet packages I've created and it brings together the three endpoints I've already mentioned. The idea is that you take your raw input and pass that into the Endpoint class that works with that input type. As output you get a SlackInformation object that looks like this

public class SlackInformation
{
    public SlackRequestType Type { get; set; }
    public Event Event { get; set; }
    public InteractionPayload Interaction { get; set; }
    public SlashCommand Command { get; set; }
}

SlackRequestType is an enum as for which type of endpoint communication was found - as well as

  • NotVerifiedRequest - We found the necessary slack headers but were unable to verify them
  • UnknownRequest - We couldn't find a slack request to verify

For now I've created 2 endpoints libraries

As I wanted to use Azure I'll be using the HttpRequest library

Setting up my Azure Function with Endpoint.HttpRequest

My IDE of choice is Visual Studio so I went into VS2019 and created a new project: File->New->Azure Function

A screenshot showing the options I selected for my new Azure Function project

Slack sends all the requests over HTTPS so we needed an HTTP Trigger and as we verify the requests based on the signing secret we need the authorization to be anonymous. This gives me a static method in a static class with template code.

First thing I want to do (after installing the NuGet Package ) is change the static class. Ideally I want to create the Endpoint process once on startup and just have every invocation of my method use that same class to verify and parse the input. So I end up with a class like this.

public class Function1
{
    public Function1()
    {
        var secret = Environment.GetEnvironmentVariable("signingSecret");
        Endpoint = new HttpRequestEndpoint(secret);
    }

Now to the method itself.

Well actually - once I've added the NuGet package. There's not a lot I have to do here. The function is already adding passing me the HttpRequest and the Endpoint object deals with that now. So I end up with something like this

[FunctionName("slacktest")]
public async Task<IActionResult> Run(
    [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest req,
    ILogger log)
{
    var context = await Endpoint.Process(req);
    if(slackInfo.Command != null && slackInfo.Command.Command == "/azureecho"){
       return Echo(slackInfo);
    }
    ...
}

private IActionResult Echo(SlackInformation context)
{
    var message = new Message
  {
      Blocks = new List<IMessageBlock>
      {
          new Section(new MarkdownText($"{context.Command.Command.Trim('/')}..._ooooo_..._ooo_"))
      }
  };
  return Task.FromResult((IActionResult)new ObjectResult(message));
}

A simplistic example but it should work, and primarily it's about how quickly I was about to get to this point. You'll see that in the Echo method I'm returning an actual Message object - this is because I want the rich formatting that comes with using Markdown, but I could just as easily have returned an OK response and sent the message via the Web API, just about how much time you've got as Slack expects the response in about 3 seconds.

I won't go through publishing as it's mostly waiting. But once I've published the function into Azure there's two things I have to do.

First things first, got to go into my Slack app and set up this new azureecho command

A screenshot showing the slack command setup

The best bit about this is there's no "what endpoint did I set up for commands, or was that events?" there's just one URL

https://slackblogtest.azurewebsites.net/api/slacktest (I deleted all my resources before I published the article, don't worry)

Then while I'm in the Slack app I'm going to grab my signing secret, because the second thing I have to do is place that in the environment variables of my Azure Function

A screenshot showing the slack command setup

N.B. Signing secrets should be treated like passwords, I put it directly as an environment variable for speed of testing a dummy app - for production I'd have secured it somewhere like Azure Key Vault

Okay. So I have the command, I have the function, now all that's left to do is test my app. I go to Slack and enter "/azureecho" into the chat box and....

A screenshot showing the slack command setup

Result! Okay it doesn't do much but you still got to a working app pretty fast! And the advantage now is that if you add a new modal that's just another if statement.

And if you add submitting the modal, maybe a global shortcut, the response to a few buttons....

Okay we got it working fast, but it's gonna get messy fast too! It's all in one place, but it's....well...all in one place!

If only I'd had to figure out something like before....hmmm....

Slack.NetStandard.RequestHandler

Oh yeah, I did! So a pattern that I had to recreate a while back was with the Alexa.NET packages I was working on. The problem was that when you had a single endpoint for all your Alexa requests, it quickly got difficult to maintain, messy. So I created a library that was the .NET equivalent of what the Amazon team used. Request Handlers.

The idea is that each of the interactions your application can handle is placed in its own class. These classes are then registered against a processing object - and that object is the one you place inside your method. Not only does this allow you to separate the Azure Function from its logic, but it means you have cleaner dependency injection and more modular functionality.

It worked so well with Alexa, I created a NuGet package to make it work with Slack and our SlackInformation object.

So here's the base interface of a Request Handler

public interface ISlackRequestHandler<TResponse>
{
    bool CanHandle(SlackContext context);
    Task<TResponse> Handle(SlackContext context);
}

The logic works that the process checks each CanHandle method in turn until one of them returns true, then runs the Handle method for that handler. This means that the ordering of the handlers allows you to go from the most specific to least specific in terms of handling responses (super useful when you have optional fields etc). TResponse means that the request handlers can be aimed at any of the Endpoint supported interactions, Azure or AWS.

It's good to know this exists, but you don't need to implement it as there are some handy helper classes you can inherit from instead. You may also notice that the interface mentions SlackContext rather than SlackInformation. SlackContext is a wrapper around SlackInformation with a couple of extra fields for advanced scenarios, but it works the same way and the method used in the AzureFunction still expects a SlackInformation object.

So first things first, we add the NuGet package Slack.NetStandard.RequestHandler and then we create the request handler that's going to replace that Echo method we had.

public class EchoCommand : SlashCommandHandler<IActionResult>
    {
        public EchoCommand() : base("azureecho")
        {
        }

        public override Task<IActionResult> Handle(SlackContext context)
        {
            var message = new Message
            {
                Blocks = new List<IMessageBlock>
                {
                    new Section(new MarkdownText($"{context.Command.Command.Trim('/')}..._ooooo_..._ooo_"))
                }
            };
            return Task.FromResult((IActionResult)new ObjectResult(message));
        }
    }

We're dealing with a simple Azure Function, so we want IActionResult as the generic parameter, but then other than that it's basically the same method just in a different place. We've removed the need for an if statement as that's handled by the fact this is a SlashCommandHandler so it knows what it's looking for (one of the helper classes I mentioned). Definitely feels cleaner.

Right - so how do we get the function to use this handler then?

First of all we need to create this new processing object that we register our handlers against. That's called a SlackPipeline and we're going to put that in the constructor after we've created the Endpoint object.

N.B. If this were something I planned to keep around for a while I'd probably move the Pipeline into a utility method away from the Function class - this way I can alter the Slack logic without ever having to make changes to the Function invocation that is currently working so well.

public Function1()
{
    var secret = Environment.GetEnvironmentVariable("signingSecret");
    Endpoint = new HttpRequestEndpoint(secret);

    var handlers = new ISlackRequestHandler<IActionResult>[]
    {
        new EchoCommand()
    };
    Pipeline = new SlackPipeline<IActionResult>(handlers);
}

Now I have this new pipeline object, I can remove the logic from the function, so the result from the Endpoint goes straight into the Pipeline

[FunctionName("slacktest")]
public async Task<IActionResult> Run(
    [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest req,
    ILogger log)
{
    var context = await Endpoint.Process(req);
    return await Pipeline.Process(context);
}

So two lines of code inside the method - much cleaner than I had before and a huge step forward in terms of speed of implementation and ability to maintain longer term. Here's my whole finished Azure Function right now.

public class Function1
{
    public Function1()
    {
        var secret = Environment.GetEnvironmentVariable("signingSecret");
        Endpoint = new HttpRequestEndpoint(secret);

        var handlers = new ISlackRequestHandler<IActionResult>[]
        {
            new EchoCommand()
        };
        Pipeline = new SlackPipeline<IActionResult>(handlers);
    }

    public SlackPipeline<IActionResult> Pipeline { get; set; }

    public HttpRequestEndpoint Endpoint { get; set; }

    [FunctionName("slacktest")]
    public async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest req,
        ILogger log)
    {
        var context = await Endpoint.Process(req);
        return await Pipeline.Process(context);
    }
}

So as I build future functionality into my slack app, I can just add that to the ISlackRequestHandler array. If someone sends an invalid request none of the request handlers I have right now will fire because I'm not handling the NotVerifiedRequest type in my handler. Actually, if none of the handlers match the request found the Pipeline will raise a SlackRequestHandlerNotFoundException so for next steps I should probably just tidy that up. But after that, if I decide to start supporting events, I can take the single URL I had for slash commands and copy that into the events URL of my slack app knowing that it's going to be handled and processed correctly.

And that's that

So this was only a short delve into how to create an Azure Function based Slack app, but hopefully you can see that the packages I've written can really help you get up and running with ease and speed, and that it can also help with maintaining those larger applications moving forward. If you've got feedback please raise issues in the GitHub repos or reach to me on social media.