This is the fourth and final blog post in a series that explains an approach to implementing ASP.NET Core Web API servers and .NET clients and provides an example implementation of a service that maps URL paths to information about corresponding entries in the Contentstack SaaS headless CMS. This entry describes some of the outstanding considerations and concerns regarding the implementation and provides a link to the source code.
This Web API server in this solution uses the content delivery APIs to retrieve all information about all page entries in the CMS that have URLs. If the number of page entries is in the thousands or tens of thousands, the network and memory overhead may be acceptable. In fact, it may be possible for this API to retrieve and cache entire entry data, providing a data source for the solution that caches data from the CMS. To load entire entries, consider the Contentstack synchronization APIs rather than the standard content delivery APIs. Beware that synchronization APIs return data about all entries rather than only those with URLs. It may be necessary to consider the order of data returned from the synchronization APIs, such as deletions and changes to URLs.
The main functions needed map URL paths to entry IDs and parse URL paths to determine relationships between entries. The data structures for information about entries are simple, storing the content type, ID, URL path, navigation title, and potentially cached JSON. For simplicity, this solution uses one data structure to map URL paths to entry IDs and for determining relationships and another data structure to map entry IDs to entry records. This is almost exactly like the logic for working with folder paths as I have previous implemented for a media importer but using URL paths from entries instead of folder structures.
Something must clear the server’s cache of data from the CMS. There are numerous strategies, such as intercepting web hooks or periodically polling the synchronization API to see if anything has changed. It is important to avoid rebuilding the cache too frequently, for example 100 times while publishing 100 entries. It is important to have a manual option to rebuild caches such as this.
This uses separate threads to load entries for each content type, and separate threads for each page within each content type. As the API logic depends on its full cache, it is important to prevent access to the cache until all threads complete. This implementation loads the cache at access, rather than at application instantiation, but the first call to the service populates the entire cache. As an optimization, this implementation retrieves only the URLs and title fields of the entries such as for using implicit hierarchies to drive navigation.
I use the service from .NET, but it is available to any technology that can invoke an HTTPS endpoint and process the resulting JSON.
I implemented two solutions: Deliverystack to contain generic code and HeadlessArchitect uses the Deliverystack framework to enhance a solution implemented previously. HeadlessArchitect contains a single project that is basically a port of the solution described by the following blog post to the this updated infrastructure.
These are the projects in the Deliverystack solution.
- Deliverystack.Core: General infrastructure for .NET attributes, threading, and CMS data modeling.
- Deliverystack.DeliveryApi: Entry URL path logic Web API server and client.
- Deliverystack.DeliveryClient: IDeliveryClient interface to implement for each CMS, file system provider, database, search index, or other repository. If you use the default ports 5000 and 5001 for a website, change the ports used for this application (I use 5550 and 5551 for the services).
- Deliverystack.RazorPages: Razor pages infrastructure for headless CMS. This project contains the single PageModelBase class used as the base class for PageModels, which contains the logic to retrieve an entry from the CMS.
- Deliverystack.StackContent: Contentstack IDeliveryClient implementation.
Naming is hard. I named my Web API project DeliveryApi without even realizing it could cause confusion with the interface/class/project with names like DeliveryClient that abstracts the CMS HTTPS API. I don’t feel like refactoring further at this time.
I run the server project from Windows Terminal. The text not selected in this image, which does not appear until the client builds its cache, shows the server requesting the list of content types and then all entries for all content types with URLS from the CMS.

You can call the API from a browser or Postman.

If you leave it enabled in development environments, you can use swagger.

After the Web API us running, you can run the HeadlessArchitect website that uses it. This is the same as the previous version, but the URL is friendlier.

Here is the same page with the json query string parameter set to true.

If you feel like trying to run this, the code contains appsettings.json files with Contentstack access credentials that will work until I change them in my stack.
Code not intended for production.
Update 13.May.2021: After writing this, I realized that the client does not support all of the API parameters, only path used in my use case (not siblings, ancestors, descendants, excludeSelf, and paging). I determined that I can use the binding model used by the Web API server in the client to pass options to the Web API – just encode the property values as a query string. This makes it important for property values to have valid default values, as all of the values appear in the URL, not just those I excplicitly set. I modified the Get() method in PathApiClient to accept a binding model instead of the string (path).
public PathApiEntryModel Get(PathApiBindingModel options) { string qs = String.Join("&", options.GetType().GetProperties().Where(p => p.GetValue(options, null) != null).Select(p => $"{Uri.EscapeDataString(p.Name)}{Uri.EscapeDataString(p.GetValue(options).ToString())}")); return JObject.Parse(_client.GetStringAsync("/pathapi?" + qs).Result).SelectToken("entries[0]").ToObject<PathApiEntryModel>(); }
Then I updated the OnGet() action method that uses the service in the PageModelBase class.
public ActionResult OnGet(string path) { if (String.IsNullOrEmpty(path)) { path = "/"; } PathApiBindingModel values = new PathApiBindingModel() { Path = path, }; PathApiEntryModel entry = _pathClient.Get(values); EntryModel = _deliveryClient.AsDefaultEntryModel( new EntryIdentifier(entry.ContentType, entry.Uid)) as PageEntryModelBase;
Also, there may be use cases where PathApiEntryModel should include the id of the parent entry.
One thought on “ASP.NET Core Web API Prototype, Part IV: Code and Concerns”