Creating a Twitter configuration provider for ASP.NET Core 1.0

Taking tweets to the next level!

 July 24, 2016


This is the first in an eventual series of posts related to ASP.NET Monster's #SummerOfConfig contest. The general idea is to implement zany configuration providers (i.e. not the kind you would expect to use in a production environment 😁 ). For my first entry, Twitter has been enlisted to provide "on-the-fly" configuration values. Imagine having the ability to change aspects of your site just by Tweeting! Read on to see how this jazz comes together.

Quick recap. how configuration works in ASP.NET Core 1.0

If an app. follows convention, the Startup constructor is where an app. will load various configuration providers using a ConfigurationBuilder instance. Most will provide an extension method which takes care of adding an IConfigurationSource to the builder. The latter exposes a Build method which returns an IConfigurationProvider instance. Now, in a controller, you can inject IConfigurationRoot and access values using IDictionary<string, string> notation.

Creating the provider

This is the easiest file to implement since it just passes an IDictionary instance to the derived ConfigurationProvider. This dictionary is what gets utilized within a controller-action to get a config. value:

using Microsoft.Extensions.Configuration;
using System.Collections.Generic;

public class TwitterConfigurationProvider : ConfigurationProvider
{
    public TwitterConfigurationProvider(IDictionary<string, string> data)
    {
        Data = data;
    }
}

Creating the source

This file contains the majority of code. Leveraging the awesome TweetinviAPI package, it is easy to authenticate and create filtered streams (push notifications from Twitter). Arguably, the Twitter-related code could go here or in the provider. Little, if anything, is gained or lost, so it is developer's preference.

Below, a stream is created leveraging the user's account and a hash tag. While the former is not necessary, it does prevent unsolicited changes to your app. The tag is just a way to filter tweets that have nothing to do with the app. When a stream-event is received, the message is parsed into key-value pairs and assigned to the same IDictionary passed to the provider.

using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using Tweetinvi;

public class TwitterConfigurationSource : IConfigurationSource
{
    public TwitterConfigurationSource(
        string consumerKey,
        string consumerSecret,
        string userAccessToken,
        string userAccessSecret,
        string hashTag)
    {
        // Null-checking omitted for brevity.

        if (!hashTag.StartsWith("#"))
        {
            throw new FormatException($"`{nameof(hashTag)}` must start with `#`. It's a Twitter hash tag.");
        }

        setupTwitterStream(consumerKey, consumerSecret, userAccessToken, userAccessSecret, hashTag);
    }

    private IDictionary<string, string> data = new Dictionary<string, string>();

    // Part of IConfigurationSource
    public IConfigurationProvider Build(IConfigurationBuilder builder)
    {
        return new TwitterConfigurationProvider(data);
    }

    private void setupTwitterStream(
        string consumerKey,
        string consumerSecret,
        string userAccessToken,
        string userAccessSecret,
        string hashTag)
    {
        Auth.SetUserCredentials(consumerKey, consumerSecret, userAccessToken, userAccessSecret);

        var user = User.GetAuthenticatedUser();

        if (user == null)
        {
            throw new InvalidOperationException("Check Twitter credentials. They do not appear valid.");
        }
        else
        {
            var stream = Stream.CreateFilteredStream();

            stream.AddTrack(hashTag);

            // We want the stream to only contain the current user's Tweets.
            stream.AddFollow(user);

            stream.MatchingTweetReceived += (sender, args) =>
            {
                // Get the whole message sans hash tag.
                var unParsedConfigurations = args.Tweet.FullText;
                var hashTagIndex = unParsedConfigurations.IndexOf(hashTag, StringComparison.InvariantCultureIgnoreCase);

                unParsedConfigurations = unParsedConfigurations.Remove(hashTagIndex, hashTag.Length)
                    .Replace(hashTag, "")
                    .Trim();

                foreach (var unParsedConfiguration in unParsedConfigurations.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries))
                {
                    /**
                     * Matches anything to the left and right of the equals-sign as long as
                     * there are no spaces between the equals-sign.
                     * `test=value` works, but `test= value`, `test =value`, and `test = value` do not.
                     */
                    var tokenMatch = Regex.Match(unParsedConfiguration, "^([^=]+)=([^ ].*)$");

                    if (tokenMatch.Success)
                    {
                        data[tokenMatch.Groups[1].Value] = tokenMatch.Groups[2].Value;
                    }
                }
            };

            // Using the non-async method causes the thread to lock and the pipeline cannot process requests!
            stream.StartStreamMatchingAllConditionsAsync();
        }
    }
}

Add the extension

Most configuration addons use an extension to keep code in the Startup minimal. This follows that convention:

using Microsoft.Extensions.Configuration;

public static class TwitterConfigurationExtensions
{
    public static IConfigurationBuilder AddTwitter(
        this IConfigurationBuilder configurationBuilder,
        string consumerKey,
        string consumerSecret,
        string userAccessToken,
        string userAccessSecret,
        string hashTag)
    {
        return configurationBuilder.Add(
            new TwitterConfigurationSource(consumerKey, consumerSecret, userAccessToken, userAccessSecret, hashTag));
    }
}

Using the extension

Because we have config. values we need to pass into our provider, we create a separate IConfigurationRoot which gets values from simple config. files.

public class Startup
{
    public Startup(IHostingEnvironment env)
    {
        var twitterConfiguration = new ConfigurationBuilder()
            .SetBasePath(env.ContentRootPath)
            .AddJsonFile("twitter.config.json")
            .AddJsonFile($"twitter.config.{env.EnvironmentName}.json", true)
            .Build();

        var builder = new ConfigurationBuilder()
            .SetBasePath(env.ContentRootPath)
            .AddTwitter(
                twitterConfiguration["consumerKey"],
                twitterConfiguration["consumerSecret"],
                twitterConfiguration["userAccessToken"],
                twitterConfiguration["userAccessSecret"],
                twitterConfiguration["hashTag"])
            .AddEnvironmentVariables();

        Configuration = builder.Build();
    }

    public IConfigurationRoot Configuration { get; }
}

twitter.config.json

{
    "consumerKey": "",
    "consumerSecret": "",
    "userAccessToken": "",
    "userAccessSecret": "",
    "hashTag": "#SummerOfConfig"
}

And, ta-da!

Screen capture showing Twitter config. provider in action

That is all for now…

Teaser for the next provider: GPS plus mobile equals progress bar!