Creating a Twitter configuration provider for ASP.NET Core 1.0
Taking tweets to the next level!
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.
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.
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;
}
}
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();
}
}
}
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));
}
}
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; }
}
{
"consumerKey": "",
"consumerSecret": "",
"userAccessToken": "",
"userAccessSecret": "",
"hashTag": "#SummerOfConfig"
}
Teaser for the next provider: GPS plus mobile equals progress bar!