Kentico Kontent Delivery .Net SDK Quick Tip: Synchronous Call
Introduction
While working on a few different Kentico Cloud based projects, I ran into a situation that was a real head scratcher when working with the Delivery .Net SDK. I had done the research, installed the fabulous Kentico Cloud Boilerplate for ASP.NET, followed the getting started examples to a T, and for the most part everything worked perfectly. However, every once in a while the whole application came to a screeching halt, with a deep level exception being thrown the very first time I ran the project. Hitting refresh always fixed the error, but the whole situation really made no sense to me at first.
I know what you must be thinking, "Well Brian, it's probably just a caching issue if it works on a refresh". Trust me, I wish it was that, but it isn't.
The Setup
As I mentioned in the introduction, my go to starting point for Kentico Cloud projects is the community based Kentico Cloud Boilerplate for ASP.NET Core MVC. It really is a well done sample project to get you started with using Kentico Cloud in the .Net world. It comes pre-packaged with the Kentico Cloud Delivery SDK for .Net. Basically the SDK is a pre-built set of APIs to communicate with the Kentico Cloud Delivery REST API service.
However, in my opinion, it has a bit too much logic in the Controller layer of the architecture that the solution presents. I was convinced of this by my lead MVC engineer at BizStream, Budd Wright. For this reason, at BizStream we normally add in another layer called the Populator layer. The job of this layer in BizStream MVC projects is to interact with the Service layer and abstract away any logic from the Controller. I essence we like our Controllers as slim (and as dumb) as possible.
Just to make this comparison make sense. Below is the code of the base boilerplate project:
using CloudBoilerplateNet.Models; using KenticoCloud.Delivery; using Microsoft.AspNetCore.Mvc; using System.Threading.Tasks; namespace CloudBoilerplateNet.Controllers { public class HomeController : BaseController { public HomeController(IDeliveryClient deliveryClient) : base(deliveryClient) { } public async Task<ViewResult> Index() { var response = await DeliveryClient.GetItemsAsync( new EqualsFilter("system.type", "article"), new LimitParameter(3), new DepthParameter(0), new OrderParameter("elements.post_date") ); return View(response.Items); } } }
And below is the BizStream version:
using KenticoCloud.Delivery; using Microsoft.AspNetCore.Mvc; using System.Threading.Tasks; using CaseStreamNet.Populaters; namespace CaseStreamNet.Controllers { public class HomeController : BaseController { public HomeController(IDeliveryClient deliveryClient) : base(deliveryClient) { } public async Task<ViewResult> Index() { return View(await new HomePagePopulater().PopulateContent(DeliveryClient)); } } }
Now this is about as simple of an example as you could find. However, it illustrates our belief that logic like finding the top 3 newest articles to show on a View, will most likely be used in other parts of the website. Having it in the Controller just doesn't make it that easy to reuse. That is where our Populater layer comes in to play. We basically move that logic into a layer that could be re-used without having to modify the Controller and possibly the View or ViewModel layer to simply reuse the call.
The PopulateContent call looks like the following below (remember I am switching up examples, the code below doesn't get 3 articles, it gets the specific homepage content from the Deliver API and its related meta information which is why it is in the HomePagePopulater).
using System.Linq; using System.Threading.Tasks; using KenticoCloud.Delivery; using CaseStreamNet.Models; using CaseStreamNet.ViewModels; namespace CaseStreamNet.Populaters { public class HomePagePopulater : BasePopulater<HomePageViewModel> { public async Task<HomePageViewModel> PopulateContent(IDeliveryClient DeliverClient) { base.Populate(DeliverClient); //Call Kentico Cloud Deliver API to get main home page content var response = await DeliverClient.GetItemAsync<HomepagePage>("homepage"); ViewModel.Content = response.Item; ViewModel.Meta = ViewModel.Content.HomepageMetaInformation.Cast<PageMetaMeta>().First(); return ViewModel; } } }
The Problem
Like I said, most of the time, this all ran great. I would hit f5 to start debugging, the home page of the site would come up. Everything was groovy. I even had continuous integration setup with an automated deployment to an Azure App Service that was hosting the staging site. However, one out of every 10 times, I would not see a home page load up, and instead I would see an exception that looked like this:
This was pretty strange since 9 out of 10 times it worked just fine. At first I thought it was just an Azure App Service problem, because it seemed to happen more there. Then I started to think it was a caching problem if someone on the content side was editing the Master layout content inside of Kentico Cloud. So I ignored it for awhile... Works on my machine, right?! However then the QA and Content teams both reported the problem to me.
Ok, so remember that previous section, and how it was the setup to show you a few of the layers in the application. Hopefully it got you up to speed on the situation. If you look close in the preceding code sample, you will notice that the HomePagePopulater class inherits from a BasePopulater class. The job of the BasePopulater class is to have the set of information for the Shared Layout of the site (main logo, site title, main navigation, and footer content) and populate a strongly typed ViewModel. It also populates the shared meta fields that every page of the website needs, like the Page Title, OpenGraph, Meta Keywords, and Meta Description tags. So it is kind of an important class!
The original code looked like this:
protected async void PopulateSiteLayout() { var response = await DeliverClientBase.GetItemAsync<MasterLayout>("master"); var layout = response.Item; //Main site logo ViewModel.SiteLogo = new Image { Url = layout.SiteLogo.First().Url, AltText = layout.LogoSubText }; ViewModel.SiteName = layout.LogoText; ViewModel.SiteSubText = layout.LogoSubText; PopulateMainMenu(layout); PopulateFooter(layout); }
Basically what was happening is that since the call to get the MasterLayout content was asynchronous, it didn't necessarily have to wait to finish loading based on the await keyword. And sure enough, the MVC pipeline was continuing on without the data and compiling into the ~/Views/Shared/_Layout.cs code since the BaseViewModel was bound to it. Again this was happening not every single time, but just once in awhile. I'm guessing it was depending on how fast the call to the Deliver API returned.
The Resolution (Quick Tip)
Since most of the examples of the Kentico Cloud Delivery SDK are all done using await, it took me a minute or two to figure out how to slow it down. It turns out that the proper way to resolve the error was to make the DeliveryClient call to the populate the MasterLayout synchronously. Below is a code sample of how to do that.
protected void PopulateSiteLayout() { //Make this a synchronous call to ensure the base stuff is loaded before rendering it to Layout Task<DeliveryItemResponse<MasterLayout>> response = Task.Run<DeliveryItemResponse<MasterLayout>>( async () => await DeliverClientBase.GetItemAsync<MasterLayout>("master") ); var layout = response.Result.Item;
So as you can see above, wrapping the call in a Task.Run statement did the trick. Using this method forces the runtime into making the asyc call happen in a synchronous way. After applying this change the runtime exception has never happened again. A good write up on what Task.Run does can be found on Stack Overflow.
Conclusion
I'm sure there might be other ways to handle this in ASP.NET MVC. However, this method seemed to work well for me. Hopefully it helps other developers out who are looking to use Kentico Cloud and the Kentico Cloud Delivery SDK for .Net. That's all for today.