Integrating Slack Notifications into a C#/.NET Core application

A beginner’s guide to integrating Slack with a C#/.NET Core application

Caleb LeNoir
6 min readJan 14, 2020

See the example project on GitHub.

First, a little background. I started as a back end developer a new company in February. It was my first time using C# since I took a week long training course about 8 years ago. Overall, I have really enjoyed working with it. There are still some things I don’t understand, but it has not been too difficult for me to pick up and run with.

In general, I do not like more notifications. I actually turn Slack off for large chunks of the day so I can focus on getting work done. I am in favor of useful communication though. And there are some cases where it is the best solution. For instance, it is important for the customer service team to know when a document has been submitted that needs to be reviewed. The endpoint has fairly low traffic (10–15 submissions a day) and it makes a large impact to be able to respond immediately instead of having to jump to a different interface to see if new requests had come through.

Building out a custom Slack app to post a notification to an internal channel seemed like the best solution for the short term. And, after implementing, it has been a huge improvement for the customer service team. In the future, if we begin to get a lot more submissions, we will need to move to a different solution, but it works well for now.

Setup a Slack App

Go to https://api.slack.com/apps?new_app=1

Create a Slack App modal

Fill in your app name, select your workspace, and create the app.

Add features and functionality to your Slack app

Under Add features and functionality, select Incoming Webhooks.

Then, create two webhooks, one for testing and one for the production notification channel. In Slack, add the two channels. I set the testing one to private so it would not show up for anyone other than me. People are interrupted enough without me adding to the noise. Use the provided curl command to test out the webhook and make sure it is showing up correctly in your slack channel.

curl -X POST -H 'Content-type: application/json' --data '{"text":"Hello, World!"}' https://hooks.slack.com/services/XXXXXXX/XXXXXXXX/XXXXXXXXXXXXXXXX

Once that is coming through, it’s time to start coding!

Our First Test

With the curl command coming through correctly, my next step was to setup a simple test from within our project that could replicate the same message. After a quick search, I found this Gist: https://gist.github.com/jogleasonjr/7121367 which included a SlackClient class and an example Test. I modified it slightly to pull the slack URL from an environment variable.

public class SlackClient : ISlackClient
{
private readonly Uri _uri;
private readonly Encoding _encoding = new UTF8Encoding();

public SlackClient()
{
_uri = new Uri(Environment.GetEnvironmentVariable("SLACK_URL"));
}
//Post a message using simple strings
public string PostMessage(string text, string channel = null)
{
var payload = new Payload
{
Channel = channel,
Text = text
};
return PostMessage(payload);
}
//Post a message using a Payload object
private string PostMessage(Payload payload)
{
var payloadJson = JsonConvert.SerializeObject(payload);
using (var client = new WebClient())
{
var data = new NameValueCollection {["payload"] = payloadJson};
var response = client.UploadValues(_uri, "POST", data); //The response text is usually "ok"
return _encoding.GetString(response);
}
}
}
//This class serializes into the Json payload required by Slack Incoming WebHooks
public class Payload
{
[JsonProperty("channel")]
public string Channel { get; set; }
[JsonProperty("text")]
public string Text { get; set; }
}
public interface ISlackClient
{
string PostMessage(string text, string channel = null);
}

In Startup.cs I added the following line to ConfigureServices so the SlackClient would be injected as a service:

services.AddTransient<ISlackClient, SlackClient>();

Full disclosure: I’m still a little unsure about service injection and exactly how it works.

Once I added the class into our Infrastructure folder and injected ISlackClientas a dependency, I was ready to create an integration test. In our IntegrationTest project, I created the test:

public class PostMessageShould
{
[Test]
public void SendToSlack()
{
var client = new SlackClient();
var result = client.PostMessage(
"THIS IS A TEST MESSAGE!");

Assert.AreEqual("ok", result);
}
}

This requires setting up an SLACK_URL in your environment. To do that, open up a terminal and run:

export SLACK_URL="<insert slack url here>"

Loading the URL through the environment allows me to setup a separate environment for testing that uses my test Slack channel and avoids putting the URL into the code itself.

After loading my URL into my environment , I ran the test and saw THIS IS A TEST MESSAGE! show up in my Slack channel. This is always remarkably satisfying to me. I ran the test a few more time, just for good measure. :)

Expanding to a Unit Testable SlackService

Next, in our Services folder, I created the SlackService and ISlackService interface. We keep both of these in the same file, but I have heard it is good practice to separate them. We might move to this in the future, but for now it works.

public class SlackService : ISlackService
{
private readonly ISlackClient _slackClient;
public SlackService(ISlackClient slackClient)
{
_slackClient = slackClient;
}
public async Task<string> PostToSlack(string message)
{

var response = _slackClient.PostMessage(message);
return response;
}
}
public interface ISlackService
{
Task<string> PostToSlack(string message);
}

With the service and interface specified, we can now create the Unit Tests. In our Setup, we mock ISlackClient, so we can test our SlackService and make sure it is working properly. Unit Tests should only ever test a single class. The mock allows us to specify what we get back from SlackClient.

private Mock<ISlackClient> _mockSlackClient;
private SlackService _slackService;
[SetUp]
public void SetUp()
{
_mockSlackClient = new Mock<ISlackClient>();
_slackService = new SlackService(_mockSlackClient.Object);
}

Then, in our test, we call SlackService.PostMessage and verify that it calls the SlackClient correctly.

[Test]
public async Task CallSlackClientPostMessage()
{
_mockSlackClient.Setup(x => x.PostMessage(It.IsAny<string>(), null)).Returns("ok");
await _slackService.PostToSlack("Test Message");

_mockSlackClient.Verify(x => x.PostMessage(It.IsAny<string>(), null), Times.Once);
}

The _mockSlackClient.Setup call tells the Mock to return "ok" whenever SlackClient.PostMessage is called. Once we have set this up, we call _slackService.PostToSlack which internally makes the call to SlackClient.PostMessage. Afterward, we use _mockSlackClient.Verify to verify the PostMessage function was called a single time. We pass It.IsAny<string>() as the first argument so it will verify if the function is called with any string. To make it more specific, we could use x.PostMessage("Test Message",null) to verify the specific string we passed into the service was passed through to the SlackClient correctly.

Finishing things up

For this, I created a function within the DocumentService:

private async Task PostSlackNotification(Document document)
{
var message = $"New Document submitted for `{document.Name}`";
await _slackService.PostToSlack(message);}

This gets called every time a document is submitted to the API endpoint. And that’s it!

How could we make it better?

The notifications are fully functional at this point, but we could run into two pretty large issues:

  1. What if the post to Slack is slow? We are degrading the user experience in order to improve things for our internal team. This is not great.
  2. What if the post to Slack fails? We would still save the document, but we would return an error to the user. This is bad!

There are few options for how to resolve these issues. A quick solution to the second would be to surround our calls to Slack in a try...catch to make sure we don’t report errors back to the user. This does not solve the issue of degraded performance though.

Another improvement we could make, would be to move the Slack calls into a background queue. This is what we ended up doing for our service.

Feedback

Thanks for reading! I would love to hear feedback or suggestions!

--

--