This post was inspired by a discussion I had recently with Chris Bass, an inspiring member of the Kentico developer community. Check out his blog for more Kentico content.
Kentico Portal Engine CMS + API
When we need to make calls to our Kentico CMS Portal Engine web application from the browser over XHR or from another web service, we need an API to communicate with.
There are several ways to accomplish this, each with pros and cons, depending on our requirements ๐ค.
Here's another great blog post about this topic from Kristian Bortnik.
Web API 2 - The Sports Car ๐
Kentico's documentation explains the steps for integrating Web API 2 into the CMS.
The approach is great if you need a large and robust custom API surface standing in front of the CMS - and it's an approach I've used many, many times ๐.
However, the setup isn't trivial and the solution effectively runs the Web API OWIN-based application within the CMS - which leads to some sharp edges ๐ช.
Kentico REST API - Public Transportation ๐
Kentico has a REST API built-in, which can be used to query, and modify all kinds of data within the application ๐ง.
It does provide security through a Basic Authentication HTTP header and but authenticates against the normal User accounts created within Kentico.
The REST API exposes the Kentico Page data and *Info
objects directly, effectively creating a projection of the database over HTTP.
Given the above, the caveat of this built-in solution is that it's verbose, not customizable, and a leaky abstraction ๐.
IHttpHandler - The Commuter Car ๐
For those simple scenarios where we only need a handful of endpoints, exposing a limited set of data, curated and filtered for us, we would like a way to build an API... without all the API.
A nice solution to this problem is the ASP.NET IHttpHandler
, which can be exposed through an .ashx
file in our CMS project.
You can read more about how IHttpHandler works in Microsoft's documentation.
IHttpHandler
gives us extremely low-level to an incoming HTTP Request and the outgoing HTTP Response.
There's no WebForms code here, just the raw request and response ๐ฎ.
This is perfect for our use-case since we don't want to render HTML through a complex web of page lifecycle events and user controls ๐.
Let's take a look at some code for a concrete example of how all of this works.
I see the
IHttpHandler
solution as ideal for small client-side integrations and also JSON-based server-to-server integrations with existing, in-production, Kentico CMS sites that can't risk big architectural changes.I've done both, but we'll look at the client-side solution today.
Example: An E-Commerce Store with Dynamic Prices
Imagine we have a Business-to-Business (B2B) e-commerce application where the prices and inventory need to be pulled live from a back-end warehousing or ERP system (not Kentico).
We don't want to delay the loading of a product details page each time a visitor requests it because we need to fetch the pricing - that would hurt SEO and the User Experience โน!
Instead, we want to cache the product details page and then, via JavaScript, request the price independently ๐.
So, we need a simple API endpoint that can forward this request on to the back-end system.
Creating the .ashx File
Let's open our Kentico CMS solution, expand the project, and then the CMSPages
folder.
Right-click on the CMSPages
folder and select "Add" -> "Generic Handler".
We're going to name this handler ProductApi
and Visual Studio will add the .ashx
extension for us.
IIS already knows how to process requests for
.ashx
files, so even if you haven't worked with this file type before, there's no extra work necessary to use it in your application ๐ค.
What we end up with is a class named ProductApi
that looks like the following:
public class ProductApi : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
context.Response.ContentType = "text/plain";
context.Response.Write("Hello World");
}
public bool IsReusable
{
get
{
return false;
}
}
}
Handling the Request
Now we need to handle the incoming request from the browser.
We have the raw HttpContext
to work with here. This is good, because we don't want the typical Web Forms infrastructure to get in our way, but it also means a bit more work for us ๐.
The ProcessRequest
method is where we will do all of our work with the HTTP Request.
Let's assume that the browser is going to send an XHR request, with a skuid
specified as a query string parameter, and it will expect a JSON response in return.
Here's ProcessRequest
with some validation and error handling:
public void ProcessRequest(HttpContext context)
{
// Set this first
context.Response.ContentType = "application/json";
string method = context.Request.HttpMethod;
if (!string.Equals(method, "GET", StringComparison.OrdinalIgnoreCase))
{
context.Response.StatusCode = 400;
return;
}
string skuIdParam = context.Request.QueryString.Get("skuid");
int skuId = ValidationHelper.GetInteger(skuIdParam, 0);
if (skuId == 0)
{
context.Response.StatusCode = 400;
return;
}
SKUInfo sku = SKUInfoProvider.GetSKUInfo(skuId);
if (sku is null)
{
context.Response.StatusCode = 404;
return;
}
// continue processing ...
Creating the Response
Now that we have handled all the potential issues, we can get our back-end system identifier out of the sku
object and request the most up-to-date values.
For our example we will pretend the response comes back in the following shape:
public class ProductStats
{
public decimal Price { get; set; }
public int Inventory { get; set; }
}
Let's get hand-wavy ๐ here and assume we were successful in getting values back from our back-end system and we now want to send them back to the browser.
These last few steps are pretty simple:
- Get the
ProductStats
response from the back-end system - Use
Newtonsoft.Json
to serialize the C# object into a JSONstring
- Write the JSON
string
as the HTTP response
// continue processing ...
ProductStats response = // response from our back-end system
string responseText = JsonConvert.SerializeObject(
response,
serializationSettings);
context.Response.Write(responseText);
}
You might have noticed the serializationSettings
parameter above. That can be customized to your preferences and use-case, but it allows you to define how Newtonsoft.Json
produces JSON from your C#.
I typically store this in a static readonly
field in my IHttpHandler
, and these are the settings I tend to use ๐:
private static readonly JsonSerializerSettings serializationSettings =
new JsonSerializerSettings
{
Formatting = Formatting.None,
ContractResolver = new CamelCasePropertyNamesContractResolver(),
// UTC Date serialization configuration
DateFormatHandling = DateFormatHandling.IsoDateFormat,
DateParseHandling = DateParseHandling.DateTimeOffset,
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
DateFormatString = "yyyy-MM-ddTHH:mm:ss.fffK",
};
Using Our API
So what does using this "API" look like?
Well, we can request the "API" in the browser like so:
But what about from JavaScript? Well, that's just as easy ๐!
I'm going to assume we're supporting modern browsers, so legacy fallbacks are up to the reader ๐:
(async () => {
const params = new URLSearchParams({ skuid: 10 });
const response = await fetch(`/CMSPages/ProductApi.ashx?${params}`);
const { price, inventory } = await response.json();
console.log('Price', price);
console.log('Inventory', inventory);
})()
Who would have thought we could create a completely custom JSON based integration API in just a couple of minutes ๐ค!?
Bonus: All the Context
I would also like to note that since the HTTP request is going to the same domain that the JavaScript loaded under, there's no annoying cookie or CORS restrictions ๐ง.
All cookies for the current domain are sent back to the server with every HTTP request, even XHR requests to our .ashx
file.
This means that the normal Kentico *Context
classes that give us access to the ambient request data, like the current authenticated user (MembershipContext
) and the current shopping cart (ShoppingCartContext
) are all still available in our ProductApi
class โก.
If we want to respond with additional discounts for Customers in different groups, or send the current user's ID with the SKU Id to our back-end system to get product recommendations, we can do that too ๐!
How about displaying a shipping time estimate based on information gathered from the Browser Geolocation API and the items in the shopping cart? Yup, we could do that ๐.
Wrap Up
While Kentico's Web API 2 integration approach and the built-in REST API provide a lot of functionality, they don't quite fit the requirements for a small, custom, minimalist endpoint exposed by the CMS for XHR requests from the browser.
Fortunately, IHttpHandler
s and .ashx
files give us a quick and dirty way to stand up an endpoint using low-level ASP.NET features, without losing out on the functionality of the CMS ๐.
If you try this approach out, let me know what you think!
Thanks for reading ๐!
If you are looking for additional Kentico content, checkout the Kentico tag here on DEV:
Or my Kentico blog series:
Top comments (0)