I was setting up an EPiServer website in Azure the other day when I ran into an issue. Whenever I tried to access edit mode a pop up would show saying:“A real-time connection could not be established with the server. This may be caused by incorrect configuration. Please see the User interface section in the user guide for further information.” It turns out that EPiServer is using websockets since one of the later version to communicate with the server when in Edit mode. Websockets are currently turned off by default in Azure WebApps, but it can be easily turned on. Just go to Settings –> Application Settings and you’ll see the following screen:

epi-websockets

Make sure to turn Web sockets on and everything should work fine again.

A difference between using a headless CMS and a traditional one is that you do not have the same control over the editing experience of the editors since the actual editing interface is in the cloud and not on premise. Does that mean that we cannot give editors custom editor controls? A while back I posted a pretty in depth article on how to extend the EPiServer editing interface with a custom control for creating polls. It took quite a lot of pain and effort to get that working. I was curious how I could achieve the same thing using Contenful and read up a bit about it on their site. It turns out it isn’t that hard at all to create your own custom edit controls in Contentful!

A word of warning!
The UI extension API is still in development and likely to change over time. It is therefore not recommended to use for business critical solutions at this time.

There’s a whole section over at the Contenful site dedicated to UI extensions which is nice. What’s even better is that it’s all open source and can be found on GitHub. What really blew me away when browsing through the examples though was this: https://github.com/contentful/widget-sdk/tree/master/examples/chessboard What!? There’s an example of an UI extension to show a chessboard! That is just beyond awesome. People that know me also know how much I love chess. If there’s one extension that every CMS needs it’s this one!

keep calm and play chess

Anyway! Lets get on with our poll extension. To get started it is easiest to download and use the CLI tool provided by Contentful. You can find it on github here. You install it through NPM (if you do not have Node or NPM you can just go to nodejs.org and download and install). Simply run npm install -g contentful-widget-cli. The global (-g) flag is to make sure the CLI is available to us globally on our machine.

Once we have the CLI tool installed we can run commands directly in our command prompt to create, read, update and delete UI extension widgets from Contentful. To do that we simply run contentful-widget [command] [options]where command can be any of create/read/update/delete and options can be a number of options for each command separately. To list available options we can run each command with the –help option.

Before we get into creating the actual widget there’s one more thing to set up. When using the CLI it will need to authenticate with Contentful to make sure you are authorized to upload and modify widgets. This is done by creating an OAUTH authentication token and setting it in an environment variable. To obtain a token you simply login to contentful and generate one here. We then set the environment variable. I did it using powershell like this: $env:CONTENTFUL_MANAGEMENT_ACCESS_TOKEN = "TOKEN"

For windows users the Contentful documentation is missing the bits of how to set up the environment variable, it simply tells you to run export CONTENTFUL_MANAGEMENT_ACCESS_TOKEN = "TOKEN" which is not a command that exists in windows. However you could do it through powershell like I did or just add the environment variable manually.

Once these few steps are done it’s time to actually create our UI extension. It will consist of only two files. Yes, you read that right… two files, and one of them is just a config widget.json file that looks like this.


{
   "id": "poll-widget",
   "name": "Poll",
   "srcdoc": "./app.html",
   "fieldTypes": ["Object"]
}

This little guy simply tells Contentful the Id of our widget, which is to identify it internally. The name, which is the name that will show up in the Contenful GUI like this:

poll-prop

Then we’ve got the srcdoc which is the actual document that will present our widget in the Contentful GUI. And finally we have an array of fieldtypes which is the type (or types) that our widget will use internally to store its data.

So that’s the configuration and then we have the actual widget, which is just a plain old html file.


<!doctype html>

<link rel="stylesheet" href="https://contentful.github.io/widget-sdk/cf-widget-api.css">

<script src="https://contentful.github.io/widget-sdk/cf-widget-api.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.js"></script>

<div class="cf-form-field">
    <label>Poll heading</label>
    <input type="text" id="poll-heading" class="cf-form-input" placeholder="Enter your poll title"/>
</div>
<div class="cf-form-field">
    <label>Poll questions</label>
    <ul id="poll-questions">
        <li>
            <input type="text" class="poll-question cf-form-input" placeholder="Add your question" />
        </li>
    </ul>
</div>

<script>
//Lets look at the script separately
</script>

A couple of interesting things here. Firstly there’s a widget-css that we can reference to make sure our widget keeps the look and feel of the Contenful GUI. There’s a fairly rudimentary style guide that you can use to view the most basic controls and classes. Then we reference the widget client library which contains the API for communicating with the contentful back end. We also reference jquery. Lastly comes the actual HTML which is very rudimentary, a textbox for the title of the poll and a textbox for the first question. The interesting stuff comes in the scripts part, here’s what it looks like:


//This is the entry point for our widgets
//The init method gets called when the widget is finished loading and ready
window.contentfulWidget.init(function(widget){

    //This is to make sure the iframe for our widget resizes appropriately when the window resizes
    widget.window.startAutoResizer();
    
    //widget.field.getValue() gets any stored value from the contentful back end and passes it to our update method
    updatePoll(widget.field.getValue());
    
    //bind any input to our savePoll method
    $('#poll-heading').on('input', function() { inputDelay(savePoll, 300); });
    
    //simple function to update our poll with any data stored 
    function updatePoll(pollData) {
        if(pollData === undefined){
            $('#poll-heading').val('New poll');
        }else {
            $('#poll-heading').val(pollData.heading);
            $('#poll-questions').html('');
            for(var i = 0; i < pollData.questions.length; i++){
                var question = pollData.questions[i];
                
                addQuestion(question.text);
                
            }
                addQuestion('');           
        }
    }
    
    //function that will be called whenever a question has changed
    function questionChanged($elem) {
        var questionVal = $elem.val();
        
        //If the value is empty we simply delete the question
        if(questionVal === '')
        {
            deleteQuestion($elem)
        }
        //If there's no question without a value, add a new one
        if($('.poll-question').filter(function() { return $(this).val() === ''; }).length === 0) {
            addQuestion('');
        }

        //save the data
        savePoll();
    }
    
    //Create an html element for the question and bind any changes to the questionChanged function
    function addQuestion(text) {
       $('<li><input type="text" class="poll-question ' + 
                'cf-form-input" placeholder="Add your question" value="' + text + '" /></li>')
                .appendTo('#poll-questions').children().on('input', function()  {
                    var that = $(this);
                    inputDelay(function() {questionChanged(that)}, 300); 
                });
    }
    
    //delete the question
    function deleteQuestion($elem) {
        $elem.parent().remove();
        
        //if there are no questions left, add a new one
        if($('#poll-questions').children().length === 0){
            addQuestion('');
        }
    }

    //save the poll to the Contentful backend        
    function savePoll(){
        //create a json object to hold our poll data
        var poll = {
            heading: '',
            questions: []
        }
        
        poll.heading = $('#poll-heading').val();
        
        $('.poll-question').each(function() {
           var questionText = $(this).val();
                  
           if(questionText){
              poll.questions.push({
                 text: questionText 
              }); 
           }
        });
        
        //this method calls into the contentful backend
        //saving our poll as a JSON object
        widget.field.setValue(poll);
    }
   
    //this is a tiny little closure hack to make sure
    //we do not send an excessive amount of events to
    //the contentful api. If the user hasn't input anything
    //for the ms number of milliseconds we call the callback
    //function, otherwise we clear it  
    var inputDelay = (function () {
        var timer = 0;
        return function (callback, ms) {
            clearTimeout(timer);
            timer = setTimeout(callback, ms);
        };
    })();
});

I first started out using blur instead of input as the event that would trigger the saving, but that turned out to be a mistake. Blur triggers when you leave an input field which sounded kind of ideal at first to minimize the number of saves triggered, but created an interesting problem. If the user changed something in the heading input field for example and then immediately clicked the publish button it would result in this error:

save-error

What happened here is that my click event on the publish button actually registered before my blur event on the input field. If I clicked on the publish button again it saved nicely, but that’s obviously not an acceptable user experience, which is why I changed it to input events instead.

Here’s what the final property looks like in the Contentful UI, also including the slug and documents properties for comparison:

poll-edit

This very simple example only scratched the surface of what you can do with the UI extensions in Contentful. The widget object for example can be used to get a reference to the entire entry that’s being edited and can modify or read any field. This means we can easily build inter-dependent widgets that listen to changes of each others values and update accordingly. I’m looking forward to playing around some more with the api in the upcoming weeks and see what else you can create.

In a previous blogpost i confessed my newfound love for Contentful. A headless CMS that exposes content through simple restful json APIs. I got so worked up about it that I decided that I needed to build something with it just to try it out. As I’m an avid fan of the asp.net community standup the choice for backend technology was easy. Frontend was another story though. I initially decided to write the app using angular, but then switched mid-building. I’ve followed the development of the Aurelia framework closely, but never really had a chance to use it in production. As it’s almost ready for a release candidate I decided to switch to it and give it a try as well.

But first things first. As I mentioned in the previous post Contentful exposes their content through a couple of different APIs, the one we will be focusing on in this instance is the Content Delivery API. This is the API responsible for delivering us our content from Contentful. It’s a fast read-only API that delivers content through a global CDN to minimize latency. You access it through simple GET requests and the response is delivered as JSON for content and as files for other content types (images, video etc.).

To be allowed access to the content you need to provide an access token with your request, either as a query string parameter or in the request header. This is a simple and flexible approach, but immediately begs the question; How do we secure our content if we have different levels of authorization for our application? For starters you should probably never expose your authentication token directly to your end user unless you’re absolutely certain that all content you have should be publicly accessible. I decided to wrap all calls to the contentful API in a controller like this:


public class ContentController : Controller
{
	private readonly string _accesstoken = "ACCESSTOKENHERE";
	private readonly string _spaceId = "SPACEID";

	public async Task<IActionResult> StartPage()
        {
            HttpClient client = new HttpClient();
            var response = await client.GetStringAsync("https://cdn.contentful.com/spaces/{_spaceId}/entries/6wUc55hRL26se2iEAS2O42?access_token=${_accesstoken}");

            return Content(response, "application/json");
        }
}

Nothing too funky going on here, we put our access token in a private variable and provide it with the request we make to the contentful API. We then return the string exactly as provided using the Content method that allows us to specify the content type of the returned response. This lets us hide the access token in our server side code that the client consuming our site content will never have to be aware of.

Lets dissect the actual call to the contentful api some more though. First we have the url to the contenful cdn: https://cdn.contenful.com/ then we have this part, /spaces/, what’s that all about?

Contentful puts all content that belongs together in a single space,it’s like your logical container for all of your content for the project. I suppose there could be instances where you would spread your content over several spaces, but the most common apporach must be: one project, one space. You will, however, share the space between different applications, so our webapplication and our android app, for example, would certainly share the same space as they would want to consume the same content. A company that has an external web and an intranet would logically have their content in two spaces, one for the content of the web and one for the intranet.

Allright, so we provide our space id to let contentful know from which space we want to fetch content, then comes the /entries/ part where we specify which entry (or entries as we shall se later on) we want to fetch. In this case I want to fetch the startpage that happens to have the id of 6wUc55hRL26se2iEAS2O42. Then we specify our access token (I’ve put it in the query string for simplicity, but you could as mentioned above hide this detail away in a header instead) and fire off the request. So we’re essentially telling contentful: Get me the entry with id 6wUc55hRL26se2iEAS2O42 from the space with id SPACEID and here’s my access token so you know I’m actually allowed to get this content.

Contentful then responds with a bunch of json, like this:


{
  "sys": {
    "space": {
      "sys": {
        "type": "Link",
        "linkType": "Space",
        "id": "SPACEID"
      }
    },
    "id": "6wUc55hRL26se2iEAS2O42",
    "type": "Entry",
    "createdAt": "2016-04-16T12:47:06.483Z",
    "updatedAt": "2016-04-22T18:40:45.159Z",
    "revision": 3,
    "contentType": {
      "sys": {
        "type": "Link",
        "linkType": "ContentType",
        "id": "startPage"
      }
    },
    "locale": "sv-SE"
  },
  "fields": {
    "title": "This is the startpage",
    "body": "This is the body text."
  }
}

Lets quickly go through the anatomy of this response. First we have the “sys” part which contains system defined metadata of the entry. In this case which space it belongs to, the id and locale of the entry, when it was created and updated and so on. The space in turn has it’s own system defined metadata, such as the space id etc. Then there’s the “fields” part that contains the actual fields we’ve created for this contentype. This is the actual content that the application needs to present.

If we now needed to secure certain content we could put up a controller action that requires authorization. Like this:


[Authorize]
public async Task<IActionResult> SecureArticles()
{
	HttpClient client = new HttpClient();
	var response = await client.GetStringAsync($"https://cdn.contentful.com/spaces/{_spaceId}/entries/?access_token={_accesstoken}&content_type=secureArticlePage&include=1");

	return Content(response, "application/json");
}

This is for the secure articles of the application. Here we do not provide an id for the entry, instead we specify that we want all the entries with the contenttype of secureArticlePage. We also provide an additional query string parameter of include=1 this means that we expect contentful to return not only the entries but also all the related assets of each entry. The number indicates how many number of levels of related assets to resolve, up to a maximum of 10. The default is 1, which means we could actually omit it in this case, but I left it there for clarity. This could be a potential security concern for us though, as we might accidentally fetch related assets that needs authorization with an unauthorized request. If for example our startpage had links to one of our secure article pages it might get returned in the json response. To make sure that no related assets is returned we need to specifically set the include query string parameter to 0. This should be done for all requests where you might potentially have sensitive assets linked from an unsecured entry.

When completed our content controller would contain the following actions for this simple app:


	public async Task<IActionResult> StartPage()

	public async Task<IActionResult> Articles()
        
	public async Task<IActionResult> Article(string id)
       
	public async Task<IActionResult> SubArticle(string id)
        
	[Authorize]
	public async Task<IActionResult> SecureArticle(string id)
        
	[Authorize]
	public async Task<IActionResult> SecureArticles()

I decided to split my content in a couple of contenttypes: startpage, article, subpage and securearticle. There are of course other ways to do this, you could for example have one contenttype and differentiate them with fields such as “secure”, “subpage” etc. I felt that for the sake of simplicity and this very basic app, it would be prudent to have them as different content types, even though they are very similar structurally.

With our back end setup it’s time to focus on getting some content out and present it to the user. As mentioned above I decided to go with Aurelia as a front end framework as I really love the simple conventionbased approach they take when it comes to modeling our views.

My index.cshtml for my homecontroller looks something like this:


<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] &mdash; Cool site</title>

    <environment names="Development">
        <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
        <link rel="stylesheet" href="~/lib/font-awesome/css/font-awesome.css" />
        <link rel="stylesheet" href="~/css/site.css" />
    </environment>
    <environment names="Staging,Production">
        <link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.5/css/bootstrap.min.css"
              asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
              asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute" />
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.1/css/font-awesome.min.css"
              asp-fallback-href="~/lib/font-awesome/css/font-awesome.min.css" asp-fallback-test-class="fa" 
              asp-fallback-test-property="font-family" asp-fallback-test-value="FontAwesome" />
        <link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
    </environment>
</head>
<body aurelia-app>

    
    <environment names="Development">
        <script src="~/lib/jquery/dist/jquery.js"></script>
        <script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
        <script src="jspm_packages/system.js"></script>
        <script src="config.js"></script>
        <script>
        System.import("aurelia-bootstrapper");
        </script>
        <script src="~/js/site.js" asp-append-version="true"></script>
    </environment>
    <environment names="Staging,Production">
        <script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.1.4.min.js"
                asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
                asp-fallback-test="window.jQuery">
        </script>
        <script src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.5/bootstrap.min.js"
                asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.js"
                asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal">
        </script>
        <script src="jspm_packages/system.js"></script>
        <script src="config.js"></script>
        <script>
        System.import("aurelia-bootstrapper");
        </script>
        <script src="~/js/site.min.js" asp-append-version="true"></script>
    </environment>
</body>
</html>

As you can see there’s basically nothing in there. Just a few tag helpers for scripts in different environments. Then there’s the hook into aurelia, the attribute aurelia-app on the body tag. This tells aurelia where we want our application to be loaded. By convention it will look for an app.js and an app.html file in the root of your application. The app.html will contain our navigation for the application and a place to show our content, you could think of it in terms of a _layout.cshtml or a masterpage. Lets start by having it display all of our articles in a navigation layout. Here’s how our app.js and app.html could look in a first simple version:


<template>
<div class="container body-content">
        <div class="row">
            <div class="col-sm-3 hidden-xs">
                <ul class="nav nav-pills nav-stacked">
                    <li role="presentation">
                        <a href="#/">Home</a>
                    </li>
                    <li role="presentation" repeat.for="article of articles">
                        <a route-href="route: articles; params.bind: {slug: article.fields.slug}">${article.fields.title}</a>
                    </li>
                </ul>
            </div>
            <div class="col-sm-9">
                <div if.bind="router.isNavigating" class="text-center">
                    <span class="fa fa-spinner fa-spin fa-4x"></span>
                </div>
                <router-view></router-view>
            </div>
        </div>
    </div>
</div>
</template>


import {inject} from 'aurelia-framework';
import {HttpClient} from 'aurelia-fetch-client';
import 'fetch'

@inject(HttpClient)
export class App {
    articles = [];

    constructor(http) {
        http.configure(config => {
            config
                .useStandardConfiguration()
                .withBaseUrl('/');   
        });

        this.http = http;
    }

    configureRouter(config, router) {
        config.title = 'This is the app title';
        config.map([
            { route: ['', 'startpage'], name: 'startpage', moduleId: './Views/startpage', nav:true, title: 'Startpage'},
            { route: 'articles/:slug', name:'articles', moduleId: './Views/article', nav:false, title: 'Article'}
        ]);

        this.router = router;
    }

    activate() {
        return this.http.fetch('content/articles')
        .then(response => response.json())
        .then(data => { 
            this.articles = data.items;
        })
    }


Aurelia supports ECMAscript 6 (and even 7) and that makes creating a view model class extremely straightforward. Simply export a class with the same name as your view and aurelia will by convention understand that they belong together. The App class first sets up the aurelia http fetch client (Aurelia recently introduced the aurelia-fetch-client as an alternative to the old aurelia-http-client , if you’re new to the fetch api I recommend this excellent blog post by David Walsh) with a baseurl. Then we have a method called configureRouter. This is a hook into the aurelia routing system to tell Aurelia what routes we will be having in our application and how we will be navigating to them. In our case we’ll start with two simple routes, one for our startpage and one for our articles. If we look closely at our app.html we can see that we have an element called <router-view> this is where the view of our current route view will be injected.

Finally we have a method called activate, this method is another hook into aurelia that will be automatically called by the routing system before presenting a view. This means that this is the perfect place to load things like remote resources that need to be available before presenting the view. In our case we call our back end controller action to fetch all of the articles from Contentful.

In our view we then iterate over all the articles and present them in a navigation. This markup


<li role="presentation" repeat.for="article of articles">
    <a route-href="route: articles; params.bind: {slug: article.fields.slug}">${article.fields.title}</a>
</li>

basically tells aurelia to repeat over the articles array from our App class and create a link for each one using the title of the article as the link text. The route-href attribute is an aurelia custom attribute that automatically creates a link to a specific route with any custom parameter values we want. We'll look at it a bit more detailed below.

Next we need to create models and views for our startpage and our articles. That is just as easy as creating our App, we just create a view/viewmodel pair. Here’s the view for the startpage:


<template>
    <require from="/components/markdown"></require>
    <div>
        <h2>${title}</h2>
        <div innerHTML.bind="body | markdown"></div>
    </div>
</template>

Very simple at this stage. Just present the title using aurelias databinding syntax. Then bind the innerHTML of a div to the body property. There’s one noteworthy thing though; Contentful sends the data of rich text fields as markdown. This means that we need to convert it back into HTML if we are to present it in our application. To do that I’ve created a very simple aurelia value-converter using showdown.js that looks like this.


import showdown from 'showdown';

export class MarkdownValueConverter {

    constructor() {
        this.converter = new showdown.Converter();
    }

    toView(value) {
        return this.converter.makeHtml(value);
    }
}

I then import the converter into our view using <require from="/components/markdown"></require> and when binding a value in aurelia you can always pass it through a value converter using the “value | converter” syntax. Which is what we do here <div innerHTML.bind="body | markdown"></div>

This means that we pass the value of our body markdown from contentful through the showdown makeHtml-method to turn it back into html before presenting it to the user.

The startpage.js file for the viewmodel looks like this:


import {inject} from 'aurelia-framework';
import {HttpClient} from 'aurelia-fetch-client';
import 'fetch'

@inject(HttpClient)
export class startPage {
    title = '';
    body = '';

    constructor(http) {
        this.http = http;
    }

    activate() {
        return this.http.fetch('content/startpage')
        .then(response => response.json())
        .then(data => {
            this.title = data.fields.title;
            this.body = data.fields.body;
        })
    }
}

No surprises here, we simply fetch the startpage from contentful in our activate method and set the title and body properties accordingly.

We should next implement a simple view for our article and I’m actually planning on using the same view for subpages and securearticlepages as well. Here’s the view:


<template>
    <require from="/components/markdown"></require>
    <require from="/components/filesize"></require>
    <require from="/components/filetype"></require>
    <div>
        <h2>${title}</h2>
        <div innerHTML.bind="body | markdown"></div>
    </div>

    <table if.bind="documents.length" class="table table-bordered table-striped">
        <thead>
            <tr>
                <th></th>
                <th>File</th>
                <th>Size</th>
            </tr>
        </thead>
        <tbody>
            <tr repeat.for="doc of documents">
                <th scope="row" class="text-center"><span class="fa fa-lg ${doc.fields.file.contentType | filetype}"></span></th>
                <td><a href.bind="doc.fields.file.url" target="_blank">${doc.fields.title}</a></td>
                <td>${doc.fields.file.details.size | filesize}</td>
            </tr>
        </tbody>
    </table>
</template>

Pretty much the same with the addition of two new value converters for file size and file type. And a table for displaying any assets that may be linked to the article. Here are the value converters:


export class FilesizeValueConverter {
    toView(value){
        //Check if it's a number
        if(!Number.isSafeInteger(value)){
            return value;
        }

        var byteUnits = ['kB', 'MB', 'GB', 'TB'];
        var i = -1;
        do{
            value = value / 1024;
            i++;
        }
        while(value > 1024)

        return Math.max(value, 0.01).toFixed(2) + ' ' + byteUnits[i];
    }
}

export class FiletypeValueConverter {
    toView(value){
        switch(value.toLowerCase()){
            case 'application/pdf':
                return 'fa-file-pdf-o';
            //omitted for brevity
            default:
                return 'fa-file-o'
        }

        return 'fa-file-o';
    }
}

And here’s the view model for articles:


import {inject} from 'aurelia-framework';
import {articleActivator} from 'components/articleActivator'

@inject(articleActivator)
export class article {
    title = '';
    body = '';
    subPages = [];
    documents = [];

    constructor(activator) {
        this.activator = activator;
    }

    activate(params) {
        return this.activator.activateArticle('content/article/' + params.slug, this)
    }
}

There are mainly two interesting bits here, the first is the activate method that takes a parameter this time and extracts a property called slug from it and appends it to our url for calling our back end. If you remember the routes we specified in our app.js file, this is where we actually use the parameters specified there. This is what the route for articles looked like:

{ 
    route: 'articles/:slug',
    name:'articles',
    moduleId: './Views/article',
    nav:false,
    title: 'Article'
}

And the mark-up for creating a route for an article looked like this:

<a route-href="route: articles; params.bind: {slug: article.fields.slug}">
    ${article.fields.title}
</a>

As you can see we specify the :slug as part of our route when configuring it. We then populate the slug part in our markup from the slug-property of our article. The slug property is of course defined in Contentful, there’s an actual property type for string properties that is specifically for this purpose. Just one little detail that makes me love contentful just a little bit more.

slug

Not only that but as I create my entry and set the title contentful automatically generates the slug property based on that title. Very convenient!

This slug is then used to fetch the correct entry from contentful, we simply filter the entries based on slug and returns the one that match. Lets have a look at how our back-end controller action achieves that.


public async Task<IActionResult> Article(string id)
        {
            HttpClient client = new HttpClient();
            var response = await client.GetStringAsync($"https://cdn.contentful.com/spaces/{_spaceId}/entries/?access_token={_accesstoken}&content_type=articlePage&fields.slug={id}");

            return Content(response, "application/json");
        }

The magic here is in the &fields.slug={id} this tells Contentful to filter our entries based on the supplied values of fields. We could filter on multiple fields and even on multiple values if we would like.

But lets go back to our article.js file, there was another interesting bit in there; We actually imported an articleActivator class into our article.js. This is done to be able to reuse it across our different types of articles. If we have a look at the view model for subpages we’ll see how we can leverage that.


import {inject} from 'aurelia-framework';
import {useView} from 'aurelia-framework';
import {articleActivator} from 'components/articleActivator'

@useView('views/article.html')
@inject(articleActivator)
export class subPage {
    title = '';
    body = '';
    subPages = [];
    documents = [];

    constructor(activator) {
        this.activator = activator;
    }

    activate(params) {
        return this.activator.activateArticle('content/subarticle/' + params.sub, this)
    }
}

As you can see it is basically identical with the article.js file, the only difference is that the subPage class uses the @useView decorator to tell aurelia where to look for its view. By convention it would look for a view with the same name and location as the view model, but here we specifically tell it to use the article.html view we saw earlier. We also see that it uses the same articleActivator class as the article.js file. Lets have a look at that activator.


import {inject} from 'aurelia-framework';
import {HttpClient} from 'aurelia-fetch-client';
import 'fetch'

@inject(HttpClient)
export class articleActivator {

    constructor(http) {
        this.http = http;
    }

    activateArticle(url, model) {
        return this.http.fetch(url)
        .then(response => response.json())
        .then(data => {
            model.title = data.items[0].fields.title;
            model.body = data.items[0].fields.body;

            if(data.includes && data.includes.Entry && data.items[0].fields.subPages) {

                var subIds = data.items[0].fields.subPages.map(x => x.sys.id);

                model.subPages = data.includes.Entry.filter(x => 
                        subIds.indexOf(x.sys.id) >= 0
                    );
            }
            else {
                model.subPages = [];
            }

            if(data.includes && data.includes.Asset && data.items[0].fields.documents) {

                var docIds = data.items[0].fields.documents.map(x => x.sys.id);
                
                model.documents = data.includes.Asset.filter(x =>
                        docIds.indexOf(x.sys.id) >= 0
                    );

            } else {
                model.documents = [];
            }

        })
    }
}

What we’ve done here is just moved the part that we would normally put in our activate method into its own separate class so we can reuse it across different classes. This kind of composition in conjunction with the aurelia dependency injection can become quite powerful when used correctly. The activator itself is pretty straightforward, it fetches json data from a url and sets the title, body, documents and subpages properties.

We now have one view model left to implement. The secure article. I think you’ll find it familiar.


import {inject} from 'aurelia-framework';
import {articleActivator} from 'components/articleActivator'
import {useView} from 'aurelia-framework';

@useView('views/article.html')
@inject(articleActivator)
export class securearticle {
    title = '';
    body = '';
    documents = [];
    subPages = [];

    constructor(activator) {
        this.activator = activator;
    }

    activate(params) {
        return this.activator.activateArticle('content/securearticle/' + params.slug, this)
    }
}

Identical to the subpage. Since we’ve moved everything vital into our articleactivator these classes become little more than simple model classes, just as they should be.

The last piece of the puzzle is to add routes for subpages and secure articles to our app.js file. The complete routes configuration would look something like this.


config.map([
    { route: ['', 'startpage'], name: 'startpage', moduleId: './Views/startpage', nav:true, title: 'Startpage'},
    { route: 'articles/:slug', name:'articles', moduleId: './Views/article', nav:false, title: 'Article'},
    { route: 'articles/:slug/:sub', name:'subArticles', moduleId: './Views/subpage', nav:false, title: 'Article'},
    { route: 'securearticles/:slug', name:'secureArticles', moduleId: './Views/securearticle', nav:false, title: 'Article'}
]);

We will also have to add the navigation to be able to reach those routes to our app.html file.


<ul class="nav nav-pills nav-stacked">
    <li role="presentation">
        <a href="#/">Home</a>
    </li>
    <li role="presentation" repeat.for="article of articles">
        <a route-href="route: articles; params.bind: {slug: article.fields.slug}">${article.fields.title}</a>
            <ul if.bind="article.fields.subPages && router.currentInstruction.fragment.indexOf('/articles/' + article.fields.slug) == 0">
                <li repeat.for="sub of article.fields.subPages">
                    <a route-href="route: subArticles; params.bind: {slug: article.fields.slug, sub: getSubEntry(sub.sys.id).fields.slug}">${getSubEntry(sub.sys.id).fields.title}</a>
                </li>
            </ul>
        </li>
    <li role="presentation" repeat.for="secureArticle of secureArticles" class="${router}">
        <a route-href="route: secureArticles; params.bind: {slug: secureArticle.fields.slug}">${secureArticle.fields.title} <span class="fa fa-lock"></span></a>
    </li>
</ul>

We then need to add a property for secureArticles to our app.js file secureArticles = []; and make sure to populate it in our activate method just as we did with the articles.

Bear in mind that the secure articles will only be accessible by an authenticated and authorized user as the security for them is implemented in the back end.

Whew! Alot of code, but what’s the conclusion?

This mini app took me a couple of hours to put together. Leveraging the contentful back-end with asp.net core was trivial. The challenge came more with structuring my front end. There are lots of cons with the headless CMS approach, but it does also put a whole lot of responsibility on the developer. As always with great power comes great responsibility.

The freedom and creativity this model allows is simply unparalleled and I am honestly considering migrating my blog straight over to Contentful. It wouldn’t cost me anything (as the pricing model is highly generous for small applications) and would give me an easy to reach and simple way to update the blog. However I do love Open Live Writer so I might stick to it for a bit longer…

Recently I’ve been toying around with Contentful. A CMS with a fresh new approach to content management. The CMSs I’ve been working with in the past have been of the “install this package to your solution and build your templates”-variety. For example EPiServer, Joomla, Drupal, SiteCore etc. Contentful instead introduces the idea of managing your content separately in the cloud and accessing it through simple yet powerful REST apis.

Historically if a client would want to build a new website the dialogue might go something like this: “We’ll start by defining the look and feel for a few weeks, then we’ll start building the actual website, then we’ll deploy it to staging and there you can start creating your content which we’ll then copy over to production once ready”.

The problem with this approach is that the producing of content will have to wait as it is dependent on the presentation. This is of course far from ideal and contentful presents an interesting solution to this by moving the production of content into their cloud instead of into a management system tied to the actual website presenting it.

But what is the point, you might ask, of producing content with nowhere to present it yet? There might of course be several good reasons for this but a few that immediately cross my mind are;

  • You might want to postpone the selection of technical platform, but not fall behind in the production of content.
  • You might want to create content for several channels (mobile app, web site etc.), but none of them might be production ready yet.
  • You might want to get started creating content quickly. (Who wouldn’t?)

As just about everyone that has been involved in a CMS-project can witness; The production of content almost always comes too little, too late and presents the application with problems that historically have been very hard to catch during development and testing. By putting the content in the cloud we could use the exact same content in all environments, mitigating the problems of poor test data and making sure our design holds up in a production environment from day one.

While that sounds all fine and dandy, how does it really work?

Imagine we were hired to build a simple web application that needs to be powered by a CMS; it needs a StartPage and ArticlePages, logged in users should be able to like and comment certain pages. We should also be able to categorize pages by tags. All fairly common requirements for any CMS-project today. Let’s compare our process in building such an app in EPiServer (a traditional CMS) and Contentful.

For both systems we’d first have to identify our content types which in our simplified example is rather easy: StartPage, ArticlePage, Like, Comment and Tag are all good candidates. Now here’s where our process would start to diverge. In the EPiServer case we’d start writing code to hardwire our ContentTypes to database models. Something like this:


[ContentType]
public class StartPageContentType : PageData
 {
	public string Heading { get; set; }
 }

In Contentful no code would have yet to be written. We’d simply log into the user interface and start creating our ContentTypes, like so:

startpage

To be fair it is also possible to create content types through the web interface in EPiServer, but to do that we’d have to have the site up and running already… also, nobody does that because it makes it harder to create strongly typed models for your views.

We’d also have to create a couple of properties for our content types. In the EPi case this would correlate to actual C# properties and the EPiServer edit interface would translate this to editable properties. In Contentful the administrators of the content types would have to create the properties themselves and decide how they would be presented for editing. This is done in a few steps where we first decide which type of property we want:

startpage-proptypes

In our case we might want a title field to indicate the title of the startpage, a good choice for that would be a text property.

startpage-textprop

We would then configure the look and feel of the field and add things like validation.

startpage-validations

There are a bunch of other settings as well to really configure how this property should behave while creating our content.

In the EPi case we might decide to store comments and likes in separate database (probably not the EPiServer DDS though…) or perhaps as some sort of contenttype, like a comment block.

In Contentful we’d create a few more ContentTypes and link them together using a Reference property:

ref-property

Conceptually we’re doing the same thing as in EPiServer. Every startpage/articlepage can have many comments and likes.

Once we have all this set up correctly we can start consuming our content. In the EPi-case we’d setup controllers and views to present the content using our @Html.PropertyFor helper method, in Contentful we call the contentful API and get our content returned as JSON.

There are two separate APIs that we can call:

  • Content Delivery API
  • Content Management API

As you can probably guess we call the first one (Content Delivery) when we want to present content, but not modify it in any way. The Content Management API is called when we need to add/delete/update parts of our content.

Calling the Contenful API is crazy simple, just make a get request specifying what content you want and supply a correct access token and you’re good to go.

For example to get all created articlepages for our site we might call the api something like this:

https://cdn.contentful.com/spaces/space-id-here/entries?access_token=this-is-the-accesstoken&content_type=articlePage

This would return a json object containing all of our articlepages.


{
  "sys": {
    "type": "Array"
  },
  "total": 1,
  "skip": 0,
  "limit": 100,
  "items": [
    {
      "sys": {
        "space": {
          "sys": {
            "type": "Link",
            "linkType": "Space",
            "id": "space-id-here"
          }
        },
        "id": "entry-id-here",
        "type": "Entry",
        "createdAt": "2016-04-13T17:37:35.831Z",
        "updatedAt": "2016-04-13T17:37:35.831Z",
        "revision": 1,
        "contentType": {
          "sys": {
            "type": "Link",
            "linkType": "ContentType",
            "id": "articlePage"
          }
        },
        "locale": "sv-SE"
      },
      "fields": {
        "title": "About us",
        "body": "Hello! This is the about us page."
      }
    }
  ]
}

We would then consume this JSON as we see fit and present the actual content to our visitor.

There are official SDKs available for many major languages, Ruby, Node.js, pure javascript, Swift, Android etc. There’s yet no official .NET SDK, but there’s an unofficial third part SDK that you can find here.

The pricing model is also highly attractive as compared to many of the commercial competitors. Up to 1000 content items, 3 users and 100k api-request/month is absolutely free. More than that costs from $99 to $199 a month depending on your needs. There’s also an enterprise model for companies with even greater demands.

As the api is rate limited it becomes very important to manage the amount of api-calls you make. Ideally you might want to cache the content returned from the API until they’re updated.

Heads up! As I wrote this blog post the good people of Contentful reached out to me and pointed out that they have multiple layers of internal caching. Both at the CDN level and below. They do not normally recommend that you cache at your application level as it is simply not necessary and adds unnecessary complexity. Also requests that hit the cache do not count towards your rate limit, which is fantastic! However, to make a point about the excellence of web hooks I’ll leave the short example below unaltered. But be advised that for an absolute majority of applications built on top of Contentful you would not have to implement any caching at all!

The problem with that would be that you need to be notified somehow when content is updated or created. Thankfully, being a modern and flexible platform, Contentful has embraced the concept of web hooks which make it dead simple to subscribe to events such as publishing, deleting and updating of content. We can set up a webhook for lots of different events and as the image below shows:

webhooks

This in conjunction with some clever caching would mean that you can keep the actual api-requests to a minimum while still responding instantly to content updates.

Is everything greener on the other side?

 

All in all I find the idea of separating content from presentation highly appealing and I hope that more companies start to look at this approach to content management. Not only is it beneficial for the content editors, being able to work without the interference of new code being deployed, without waiting for new properties to show up or bugs to be fixed, it is also highly beneficial for us developers; Being able to focus solely on the website/application consuming the content and not waste any time struggling with outdated CMS APIs and frustrating legacy wysiwyg editors.

That said though, change is always painful in one way or the other. I believe many EPiServer editors of today would feel a little lost without their trusty page tree representing the hierarchy of their pages. They might also miss the “on-page editing” capabilities introduced in EPiServer 7. Perhaps they’ll shed a tear for the dynamic properties, or maybe not…

I do believe, however, that they will love the lean, clean and intuitive edit interface in Contentful. The modern approach to handling digital assets (everything from markdown, to code, to video, images etc.) and the simplicity of creating content.

Change might be painful, but change is also inevitable. As the slow, ponderous behemoth CMSs of the past struggle to keep up with an ever changing and evolving web where front end frameworks have the life span of a dragonfly and major corporations change the rules for everybody, they might just see themselves get outpaced by the small and the nimble. And nimblest of them all is Contentful, CMS of the future…

I recently wrote an application where users had a need for some very specific searches. We decided to implement these searches as a query to the database. As we were using Entity Framework we first wrote our search implementation something like this:


    repository.Stuff.Where(c => c.SomeStuff.ToLower().Contains(query)) || c.SomeOtherStuff != null && c.SomeOtherStuff.Things.ToLower().Contains(query))

We had about 20ish different properties that we needed to null check and make a case insensitive search for. A couple of these properties belonged to entities in different tables than our imaginary Stuff table above and had "one to one" or "many to many" relationships.

The generated sql looked like something out of Edgar Allan Poes worst nightmares:

 
SELECT
    [UnionAll1].[Id] AS [C1],
    [UnionAll1].[Id1] AS [C2],
    [UnionAll1].[Id2] AS [C3],
    [UnionAll1].[Stuff] AS [C4],
    [UnionAll1].[Stuff2] AS [C5],
    [UnionAll1].[Stuff3] AS [C6],
    [UnionAll1].[Some_Id] AS [C7],
    [UnionAll1].[SomeOtherStuff] AS [C8],
    [UnionAll1].[Stuff666] AS [C9],
    [UnionAll1].[MoreStuff] AS [C10],
    [UnionAll1].[Things] AS [C11],
    [UnionAll1].[OtherThing] AS [C12],
    [UnionAll1].[Something] AS [C13],
    [UnionAll1].[PhoneNumberOrSomething] AS [C14],
    [UnionAll1].[AnotherThing] AS [C15],
    [UnionAll1].[MoreStuff] AS [C16],
    [UnionAll1].[AndMore] AS [C17],
    [UnionAll1].[MoreThings] AS [C18],
    [UnionAll1].[AndMoreMore] AS [C19],
    [UnionAll1].[Stuff4] AS [C20],
    [UnionAll1].[AndEvenMoreStuff] AS [C21],
    [UnionAll1].[CoolStuff] AS [C22],
    [UnionAll1].[SomeDate] AS [C23],
    [UnionAll1].[Stuff5] AS [C24],
    [UnionAll1].[Stuff6] AS [C25],
    [UnionAll1].[Stuff7] AS [C26],
    [UnionAll1].[AndSomeUserName] AS [C27],
    [UnionAll1].[BoolStuff] AS [C28],
    [UnionAll1].[BitStuff] AS [C29],
    [UnionAll1].[MoreThings] AS [C30],
    [UnionAll1].[Id3] AS [C31],
    [UnionAll1].[EvenMoreStuff] AS [C32],
    [UnionAll1].[AndMoreStuff] AS [C33],
    [UnionAll1].[Stuff8] AS [C34],
    [UnionAll1].[Stuff9] AS [C35],
    [UnionAll1].[IActuallyCantMakeUpMoreColumnNames] AS [C36],
    [UnionAll1].[Please] AS [C37],
    [UnionAll1].[No] AS [C38],
    [UnionAll1].[Moooore] AS [C39],
    [UnionAll1].[C1] AS [C40],
    [UnionAll1].[Id4] AS [C41],
    [UnionAll1].[ThisIsInsane] AS [C42],
    [UnionAll1].[StuffAgainAndAgain] AS [C43],
    [UnionAll1].[ICantTakeIt] AS [C44],
    [UnionAll1].[MakeItStop] AS [C45],
    [UnionAll1].[Almost] AS [C46],
    [UnionAll1].[JustAFewMore] AS [C47],
    [UnionAll1].[ComeOn] AS [C48],
    [UnionAll1].[AlmostThere] AS [C49],
    [UnionAll1].[Finally] AS [C50],
    [UnionAll1].[C2] AS [C51],
    [UnionAll1].[C3] AS [C52],
    [UnionAll1].[C4] AS [C53],
    [UnionAll1].[C5] AS [C54],
    [UnionAll1].[C6] AS [C55],
    [UnionAll1].[C7] AS [C56]
    FROM  (SELECT
        CASE WHEN ([Join2].[Some_Id] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1],
        [Extent2].[Id] AS [Id],
        [Extent1].[Id] AS [Id1],
        [Extent1].[Id] AS [Id2],
        [Extent1].[Stuff] AS [Stuff],
        [Extent1].[Stuff2] AS [Stuff2],
        [Extent1].[Stuff3] AS [Stuff3],
        [Extent1].[Person_Id] AS [Person_Id],
        [Extent1].[SomeOtherStuff] AS [SomeOtherStuff],
        [Extent1].[Stuff666] AS [Stuff666],
        [Extent1].[MoreStuff] AS [MoreStuff],
        [Extent1].[AndMore] AS [AndMore],
        [Extent1].[MoreThings] AS [MoreThings],
        [Extent1].[AndMoreMore] AS [AndMoreMore],
        [Extent1].[PhoneNumberOrSomething] AS [PhoneNumberOrSomething],
        [Extent1].[AnotherThing] AS [AnotherThing],
        [Extent1].[Things] AS [Things],
        [Extent1].[OtherThing] AS [OtherThing],
        [Extent1].[Stuff4] AS [Stuff4],
        [Extent1].[Stuff5] AS [Stuff5],
        [Extent1].[AndEvenMoreStuff] AS [AndEvenMoreStuff],
        [Extent1].[CoolStuff] AS [CoolStuff],
        [Extent1].[SomeDate] AS [SomeDate],
        [Extent1].[Stuff6] AS [Stuff6],
        [Extent1].[Stuff7] AS [Stuff7],
        [Extent1].[AndSomeUserName] AS [AndSomeUserName],
        [Extent1].[BitStuff] AS [BitStuff],
        [Extent1].[BoolStuff] AS [BoolStuff],
        [Extent1].[MoreThings] AS [MoreThings],
        [Extent1].[EvenMoreStuff] AS [EvenMoreStuff],
        [Extent1].[AndEvenMoreStuff] AS [AndEvenMoreStuff],
        [Extent2].[Id] AS [Id3],
        [Extent2].[MoreMoreMore] AS [MoreMoreMore],
        [Extent2].[Stuff8] AS [Stuff8],
        [Extent2].[Stuff9] AS [Stuff9],
        [Extent2].[IActuallyCantMakeUpMoreColumnNames] AS [IActuallyCantMakeUpMoreColumnNames],
        [Extent2].[Please] AS [Please],
        [Extent2].[No] AS [No],
        [Extent2].[Moooore] AS [Moooore],
        [Extent2].[ThisIsInsane] AS [ThisIsInsane],
        [Join2].[Id] AS [Id4],
        [Join2].[StuffAgainAndAgain] AS [StuffAgainAndAgain],
        [Join2].[ICantTakeIt] AS [ICantTakeIt],
        [Join2].[MakeItStop] AS [MakeItStop],
        [Join2].[Almost] AS [Almost],
        [Join2].[JustAFewMore] AS [JustAFewMore],
        [Join2].[ComeOn] AS [ComeOn],
        [Join2].[AlmostThere] AS [AlmostThere],
        [Join2].[Almoooost] AS [Almooost],
        [Join2].[Finally] AS [Finally],
        CAST(NULL AS int) AS [C2],
        CAST(NULL AS varchar(1)) AS [C3],
        CAST(NULL AS int) AS [C4],
        CAST(NULL AS varchar(1)) AS [C5],
        CAST(NULL AS datetime2) AS [C6],
        CAST(NULL AS int) AS [C7]
        FROM   [dbo].[StuffTable] AS [Extent1]
        INNER JOIN [dbo].[TableOfOtherStuff] AS [Extent2] ON [Extent1].[Some_Id] = [Extent2].[Id]
        LEFT OUTER JOIN  (SELECT [Extent3].[Other_Id] AS [Other_Id], [Extent3].[Stuff_Id] AS [Stuff_Id], [Extent4].[Id] AS [Id], [Extent4].[Stuff] AS [Stuff], [Extent4].[Morestuff] AS [MoreStuff], [Extent4].[AndMore] AS [AndMore], [Extent4].[EvenMore] AS [EvenMore], [Extent4].[MoreMoreMore] AS [MoreMoreMore], [Extent4].[MoreMoreMore] AS [MoreMoreMore], [Extent4].[ItNeverEnds] AS [ItNeverEnds], [Extent4].[ComeOnPlease] AS [ComeOnPlease], [Extent4].[Argh] AS [Argh]
            FROM  [dbo].[SomeOtherTable] AS [Extent3]
            INNER JOIN [dbo].[AndAnotherTableOfStuff] AS [Extent4] ON [Extent3].[Some_Id] = [Extent4].[Id] ) AS [Join2] ON [Extent1].[Id] = [Join2].[Some_Id]
        WHERE (([Extent1].[Stuff] IS NOT NULL) AND (LOWER([Extent1].[Stuff]) LIKE N'%rh%')) OR (([Extent1].[Stuff2] IS NOT NULL) AND (LOWER([Extent1].[Stuff2]) LIKE N'%rh%')) OR (([Extent1].[MoreStuff] IS NOT NULL) AND (LOWER([Extent1].[MoreStuff]) LIKE N'%rh%')) OR (([Extent1].[EvenMoreStuff] IS NOT NULL) AND ([Extent1].[EvenMoreStuff] LIKE N'%rh%')) OR (([Extent2].[Please] IS NOT NULL) AND (LOWER([Extent2].[Please]) LIKE N'%rh%')) OR (([Extent2].[MakeItStop] IS NOT NULL) AND (LOWER([Extent2].[MakeItStop]) LIKE N'%rh%')) OR (([Extent2].[Finally] IS NOT NULL) AND (LOWER([Extent2].[Finally]) LIKE N'%rh%'))
    UNION ALL
        SELECT
        2 AS [C1],
        [Extent6].[Id] AS [Id],
        [Extent5].[Id] AS [Id1],
        [Extent5].[Id] AS [Id2],
        [Extent5].[Stuff] AS [Stuff],
        [Extent5].[MoreStuff] AS [MoreStuff],
        [Extent5].[Stuff2] AS [Stuff2],
        [Extent5].[Some_Id] AS [Some_Id],
        [Extent5].[Stuff3] AS [Stuff3],
        [Extent5].[EvenMoreStuff] AS [EvenMoreStuff],
        [Extent5].[SomeOtherStuff] AS [SomeOtherStuff],
        [Extent5].[Stuff666] AS [Stuff666],
        [Extent5].[AndMore] AS [AndMore],
        [Extent5].[MoreThings] AS [MoreThings],
        [Extent5].[Thingy] AS [Thingy],
        [Extent5].[OtherThing] AS [OtherThing],
        [Extent5].[AndSomeUserName] AS [AndSomeUserName],
        [Extent5].[PhoneNumberOrSomething] AS [PhoneNumberOrSomething],
        [Extent5].[ItNeverEnds] AS [ItNeverEnds],
        [Extent5].[ItGoesOnAndOn] AS [ItGoesOnAndOn],
        [Extent5].[AndOnAndOn] AS [AndOnAndOn],
        [Extent5].[Please] AS [Please],
        [Extent5].[AnotherColumn] AS [AnotherColumn],
        [Extent5].[IActuallyCantMakeUpMoreColumnNames] AS [IActuallyCantMakeUpMoreColumnNames],
        [Extent5].[Stuff4] AS [Stuff4],
        [Extent5].[Stuff5] AS [Stuff5],
        [Extent5].[BitStuff] AS [BitStuff],
        [Extent5].[BoolStuff] AS [BoolStuff],
        [Extent5].[CoolStuff] AS [CoolStuff],
        [Extent5].[SomeDate] AS [SomeDate],
        [Extent5].[StuffAgainAndAgain] AS [StuffAgainAndAgain],
        [Extent6].[Id] AS [Id3],
        [Extent6].[ICantTakeIt] AS [ICantTakeIt],
        [Extent6].[MakeItStop] AS [MakeItStop],
        [Extent6].[AlmostThere] AS [AlmostThere],
        [Extent6].[JustAFewMore] AS [JustAFewMore],
        [Extent6].[ComeOn] AS [ComeOn],
        [Extent6].[Almost] AS [Almost],
        [Extent6].[Almoooost] AS [Almoooost],
        [Extent6].[Finally] AS [Finally],
        CAST(NULL AS int) AS [C2],
        CAST(NULL AS varchar(1)) AS [C3],
        CAST(NULL AS varchar(1)) AS [C4],
        CAST(NULL AS varchar(1)) AS [C5],
        CAST(NULL AS varchar(1)) AS [C6],
        CAST(NULL AS varchar(1)) AS [C7],
        CAST(NULL AS varchar(1)) AS [C8],
        CAST(NULL AS varchar(1)) AS [C9],
        CAST(NULL AS bit) AS [C10],
        CAST(NULL AS bit) AS [C11],
        [Extent7].[Id] AS [Id4],
        [Extent7].[Url] AS [Url],
        [Extent7].[SomeThing] AS [SomeThing],
        [Extent7].[MoreStuff] AS [MoreStuff],
        [Extent7].[Crazy] AS [Crazy],
        [Extent7].[StuffId] AS [StuffId]
        FROM   [dbo].[Thingies] AS [Extent5]
        INNER JOIN [dbo].[SomeOtherTable] AS [Extent6] ON [Extent5].[Some_Id] = [Extent6].[Id]
        INNER JOIN [dbo].[AndAnotherTable] AS [Extent7] ON [Extent5].[Id] = [Extent7].[StuffId]
        WHERE (([Extent5].[Stuff] IS NOT NULL) AND (LOWER([Extent5].[Stuff]) LIKE N'%rh%')) OR (([Extent5].[Stuff2] IS NOT NULL) AND (LOWER([Extent5].[Stuff2]) LIKE N'%rh%')) OR (([Extent5].[SomeOtherStuff] IS NOT NULL) AND (LOWER([Extent5].[SomeOtherStuff]) LIKE N'%rh%')) OR (([Extent5].[EvenMoreStuff] IS NOT NULL) AND ([Extent5].[EvenMoreStuff] LIKE N'%rh%')) OR (([Extent6].[Please] IS NOT NULL) AND (LOWER([Extent6].[Please]) LIKE N'%rh%')) OR (([Extent6].[NoMore] IS NOT NULL) AND (LOWER([Extent6].[NoMore]) LIKE N'%rh%')) OR (([Extent6].[Finally] IS NOT NULL) AND (LOWER([Extent6].[Finally]) LIKE N'%rh%'))) AS [UnionAll1]
    ORDER BY [UnionAll1].[Id] ASC, [UnionAll1].[Id2] ASC, [UnionAll1].[Id3] ASC, [UnionAll1].[C1] ASC

my-eyes

This literally hurts to look at... and needless to say resulted in abysmal performance.

The query in itself took about 10-15 seconds to execute over a dataset of about 15 000 records... It was time to look at the alternatives.

We immediately gave up on Entity Framework for these queries. Now, don't get me wrong, I really like Entity Framework, the problem here isn't the framework in itself it's the ungrateful queries we give it to process. As Entity Framework is a "one shoe fits all"-solution that is a great tool in a wide spectrum of use cases it simply does not perform well with specialized queries such as these. You could of course execute plain sql queries through the DbContext, but this still comes with considerable overhead and the mapping to objects is not as streamlined as one could wish.

Enter dapper

quite-dapper

Dapper is a "simple object mapper for .Net" that simply extends the IDbConnection interface and provides helper methods to execute a query or command and map the results to simple POCO .Net objects. It is extremely performant and light weight.

Our implementation with dapper took our initial query and made it look something like this:


SELECT TOP 10
StuffTable.Id,
Stuff collate sql_latin1_general_cp1_ci_as AS Stuff,
StuffTable.StuffNumber collate sql_latin1_general_cp1_ci_as AS StuffNumber,
MoreStuff collate sql_latin1_general_cp1_ci_as AS MoreStuff,
Stuff666 collate sql_latin1_general_cp1_ci_as AS Stuff666,
ThingsTable.ThingsNumber collate sql_latin1_general_cp1_ci_as AS ThingsNumber,
ThingsTable.Thingy collate sql_latin1_general_cp1_ci_as AS Thingy,
ThingsTable.OtherThing collate sql_latin1_general_cp1_ci_as AS OtherThing,
OtherTable.OtherStuff collate sql_latin1_general_cp1_ci_as AS OtherStuff,
OtherTable.EvenMoreStuff collate sql_latin1_general_cp1_ci_as  AS EvenMoreStuff,
LEFT(Thingy, 1) + ' ' + LEFT(OtherThing, 1) AS WeirdStuff
FROM dbo.StuffTable
LEFT JOIN dbo.ThingsTable ON ThingsTable.Id = Cases.Thing_Id
LEFT JOIN dbo.StuffTableToOtherTable ON StuffTableToOtherTable.Stuff_Id = StuffTable.Id
LEFT JOIN dbo.OtherTable ON OtherTable.Id = StuffTableToOtherTable.Other_Id
 
WHERE
LEFT(Thingy, 1) + ' ' + LEFT(OtherThing, 1) = 'r l' OR
Stuff LIKE '%r l%' OR
StuffNumber LIKE '%r l%' OR
MoreStuff LIKE 'r l%' OR
Stuff666 LIKE 'r l%' OR
ThingsNumber LIKE '%r l%' OR
Thingy LIKE 'r l%' OR
OtherThing LIKE 'r l%' OR
Thingy + ' ' + OtherThing LIKE 'r l%' OR
OtherStuff LIKE 'r l%' OR
EvenMoreStuff LIKE '%r l%'

Yeah, it still isn't very pretty, but lets face it; A complicated SQL query will never be more than half readable...

To execute the query with dapper we simply do something like this:


 var query = //our crazy query from above, omitted for brevity
 var dbConnection = //our database connection of type IDbConnection
 
 var res = dbConnection.Query<AwesomeResult>(query);


We execute the result on our database connection and dapper maps it automatically to our AwesomeResult class.

With this approach our once monstrously slow query took about 14ms to execute.