ASP.NET Core 2.0


ASP.NET Core 2.0 was announced 5 days ago and has received a lot of praise in the .NET community. I thought it would be fun to create a web app using the new tech; the new ASP.NET is shiny and comes with many built-in features.

Starting a new project in Visual Studio and choosing ASP.NET Core Web Application prompts the user with 3 choices; empty project, Web API and Web Application (which is MVC):

I chose the MVC variant to create a news feed web app that queries the Twitter API for news. Creating a TwitterService class I found out that Core has its own dependency injection mechanism, which is quite nice. To add services to the IoC container, simply navigate to the Startup class and register the service inside the ConfigureServices method:

public void ConfigureServices(IServiceCollection services)
{
    // Add framework services.
    services.AddMvc();
    services.AddTransient<ITwitterService, TwitterService>();
}

The service can then be injected:

private readonly ITwitterService _twitterService;

public HomeController(ITwitterService twitterService)
{
    _twitterService = twitterService;
}

As mentioned, the news feed web app will query Twitter for news. As of Twitter API 1.1, all requests to the API must be authenticated. This means that we need to send an authorization header in the request to the API containing consumer keys and access tokens. To get the keys, a Twitter app must first be created. The Twitter app can be created by visiting the Twitter application management site and clicking on Create New App:

A new app will be created with the needed keys. Using the keys, the next step is to perform the queries to Twitter. The best way to do this is on the server side. To create a query for a given Twitter screen name (user) using Core, is a bit tricky; there is quite a lot of boiler plate code necessary to get it working. The TwitterService class has a GetTweetsJson method which builds up and performs the query:

public string GetTweetsJson(string screenName)
{
    // Oauth application keys
    var oauth_token = "ACCESS_TOKEN";
    var oauth_token_secret = "ACCESS_TOKEN_SECRET";
    var oauth_consumer_key = "CONSUMER_KEY";
    var oauth_consumer_secret = "CONSUMER_SECRET";

    // Oauth implementation details
    var oauth_version = "1.0";
    var oauth_signature_method = "HMAC-SHA1";

    // Unique request details
    var oauth_nonce = Convert.ToBase64String(
        new ASCIIEncoding().GetBytes(DateTime.Now.Ticks.ToString()));
    var timeSpan = DateTime.UtcNow
                    - new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
    var oauth_timestamp = Convert.ToInt64(timeSpan.TotalSeconds).ToString();

    // Message api details    
    var resource_url = "https://api.twitter.com/1.1/statuses/user_timeline.json";
    
    // Create oauth signature
    var baseFormat = "oauth_consumer_key={0}&oauth_nonce={1}&oauth_signature_method={2}" +
                        "&oauth_timestamp={3}&oauth_token={4}&oauth_version={5}&screen_name={6}";

    var baseString = string.Format(baseFormat,
        oauth_consumer_key,
        oauth_nonce,
        oauth_signature_method,
        oauth_timestamp,
        oauth_token,
        oauth_version,
        Uri.EscapeDataString(screenName)
    );

    baseString = string.Concat("GET&", Uri.EscapeDataString(resource_url), "&", Uri.EscapeDataString(baseString));

    var compositeKey = string.Concat(Uri.EscapeDataString(oauth_consumer_secret),
        "&", Uri.EscapeDataString(oauth_token_secret));

    string oauth_signature;
    using (HMACSHA1 hasher = new HMACSHA1(ASCIIEncoding.ASCII.GetBytes(compositeKey)))
    {
        oauth_signature = Convert.ToBase64String(
            hasher.ComputeHash(ASCIIEncoding.ASCII.GetBytes(baseString)));
    }

    // Create the request header
    var headerFormat = "OAuth oauth_nonce=\"{0}\", oauth_signature_method=\"{1}\", " +
                        "oauth_timestamp=\"{2}\", oauth_consumer_key=\"{3}\", " +
                        "oauth_token=\"{4}\", oauth_signature=\"{5}\", " +
                        "oauth_version=\"{6}\"";

    var authHeader = string.Format(headerFormat,
        Uri.EscapeDataString(oauth_nonce),
        Uri.EscapeDataString(oauth_signature_method),
        Uri.EscapeDataString(oauth_timestamp),
        Uri.EscapeDataString(oauth_consumer_key),
        Uri.EscapeDataString(oauth_token),
        Uri.EscapeDataString(oauth_signature),
        Uri.EscapeDataString(oauth_version)
    );

    // Make the request
    var postBody = "screen_name=" + Uri.EscapeDataString(screenName);//
    resource_url += "?" + postBody;
    var request = (HttpWebRequest)WebRequest.Create(resource_url);
    request.Headers["Authorization"] = authHeader;
    request.Method = "GET";
    request.ContentType = "application/x-www-form-urlencoded";

    var response = request.GetResponseAsync().Result;
    var responseData = new StreamReader(response.GetResponseStream()).ReadToEnd();
    return responseData;
}

The Oauth application keys must be replaced accordingly. This method queries Twitter and returns the latest 20 tweets by a given user, as a JSON. We deserialize the JSON response and take the latest tweet:

var tweetsJson = _twitterService.GetTweetsJson("bbcnews");
var tweet = JsonConvert.DeserializeObject<List<Tweet>>(tweetsJson).First();

The Tweet class:

public class Tweet
{
    [JsonProperty("created_at")]
    public string CreatedAt { get; set; }

    [JsonProperty("id")]
    public long Id { get; set; }

    [JsonProperty("id_str")]
    public string IdString { get; set; }

    [JsonProperty("text")]
    public string Text { get; set; }

    [JsonProperty("user")]
    public User User { get; set; }

    [JsonProperty("entities")]
    public Entity Entity { get; set; }
}

We have a partial view that gets updated by an Ajax call every 5 minutes to fetch the tweets:

@using Microsoft.AspNetCore.Mvc.Rendering
@model System.Collections.Generic.IEnumerable<Tweet>

<div id="tweets">
    @Html.Partial("~/Views/Partial/_Tweets.cshtml") 
</div>

<script type="text/javascript">
    window.setInterval(function() {
        $.get('/Home/Partial', function (partialViewResult) {
            $("#tweets").html(partialViewResult);
        });
    }, 300000);
</script>

The partial view:

@model System.Collections.Generic.IEnumerable<Tweet>

@foreach (var tweet in Model)
{
    if (tweet.Entity.Urls.Count > 0)
    {
        <div class="tweet" onclick="location.href = '@tweet.Entity.Urls.First().UrlString';" style="cursor: pointer;">
            <img src="@tweet.User.ProfileImageUrlHttps"> <strong>@tweet.User.Name:</strong> @tweet.Text
            <a href="@tweet.Entity.Urls.First().UrlString">@tweet.Entity.Urls.First().UrlString</a>
        </div>
    }
    else
    {
        <div class="tweet">
            <img src="@tweet.User.ProfileImageUrlHttps"> <strong>@tweet.User.Name:</strong> @tweet.Text
        </div>
    }
}

And the server side code:

private readonly ITwitterService _twitterService;
private readonly List<string> _screenNames = new List<string>() {"bbcnews", "bbcbreaking", "bbcworld", "cnn", "cnnbrk",
    "reuters", "skynews", "washingtonpost", "ap", "guardian", "nytimes", "time", "wsj" };
private readonly List<Tweet> _tweets = new List<Tweet>();

public HomeController(ITwitterService twitterService)
{
    _twitterService = twitterService;
}

[HttpGet]
public IActionResult Partial()
{
    foreach (var sn in _screenNames)
    {
        var tweetsJson = _twitterService.GetTweetsJson(sn);
        var tweet = JsonConvert.DeserializeObject<List<Tweet>>(tweetsJson).First();
        var cleanTweet = _twitterService.CleanText(tweet);
        _tweets.Add(cleanTweet);
    }
    var sortedTweets = _tweets.OrderByDescending(x => x.Id);
    return PartialView("~/Views/Partial/_Tweets.cshtml", sortedTweets);
}

The final step is to publish our solution, this is done by right clicking the project and choosing Publish…:

Choose Microsoft Azure App Service, sign in with a Microsoft account and fill in the details:

We can then view the web app in action, in this case I published the solution to https://worldnewsfeed.azurewebsites.net/:

Feel free to use the solution, the code is found here, comments and thoughts are welcome!