.NET Core 3.1 with Kontent.ai – Part 1: Clean Architecture Design

This series will focus on giving you the tools you need to create awesome products with .NET Core and Kontent.ai.

Welcome to Part 1 of my series on .NET Core Kontent.ai websites! I love working with .Net Core and Kontent, so this series will focus on giving you the tools you need to create awesome products. Weā€™ll start at the place where all successful projects should begin from a development perspective: with a look at what type of architecture is going to position us for success based on the projectā€™s technical requirements.

Architecting your solutions properly is important. In Robert C Martinā€™s Clean Architecture, he states the goal of software architecture is ā€œto minimize the human resources required to build and maintain the required systemā€. Trying to make changes to a project that has bad architecture is a bit like gluing together your legos: worked great the first time you did it but is a real headache if you need to make any changes! šŸ˜¬

Introduction

If youā€™re like me, thereā€™s always THAT PROJECT that you dread making changes to because changing one thing is like pulling out a bottom piece on a teetering Jenga tower, one wrong move and everything comes crashing down.Ā 

Now imagine your boss saying thereā€™s a critical feature malfunctioning in THAT PROJECT and YOU need to fix it right away before the company bleeds too much money! šŸ˜µ

Eager to solve the critical problem, you rip open the codebase, take one look at the code, and the nightmares of the last time you had to change something in this project come rushing back.

Star Trek

You psych yourself up, thinking ā€œI can do this!ā€ Remembering the Jenga tower you formulate a plan of attack, in and out with lightning speed, fixing the issue quickly and efficiently with no casualties. Hereā€™s your plan for the change:

Giant Jenga

But as soon as one unforeseen force hits you, everything crumbles around you and it ends up being more like this:

Giant organe Jenga

This is what bad architecture can do to your project, it can cripple it making the easiest changes difficult and risky.

In this post, weā€™ll review the architecture of a production .Net Core 3.1 MVC website with a Kontent.ai headless CMS. Weā€™ll review a SOLID architecture thatā€™s easy to scale and maintain. Weā€™ll cover which projects youā€™ll want to add, what each project does, and why each project is important.

Normally weā€™d look at the project requirements and goals first, evaluating which technologies would be good fits for the requirements. For the purpose of this article, weā€™ll assume the technology requirements are already chosen.Ā 

Technology Requirements

Goal

The goal of this architecture is to segregate the Kentico dependencies in a repository layer so the remainder of the project has no dependencies on Kentico, the external API doubling as our data layer.Ā 

Sean Wright outlines a similar architecture for Kentico 12 MVC, N-Tier Architecture – Layers as Abstractions, in his excellent blog post, Kentico 12: Design Patterns Part 20 – Choosing a Solution Architecture.

Overview

I love all of the examples you can find online. Reading other peoplesā€™ articles and reviewing other projects on open source platforms like GitHub has really taught me a lot about what good software architecture looks like. A great example, complete with automated tests, is the Kontent.ai Boilerplate repo for ASP.NET Core MVC. Like most projects online, for simplicity, their HomeController performs all of the logic: it queries the database, creates objects, and returns the object mashed together with the view back to the browsers.Ā 

public class HomeController : BaseController<HomeController>
{
    public async Task<ViewResult> Index()
    {
        var response = await DeliveryClient.GetItemsAsync<Article>(
            new EqualsFilter("system.type", "article"),
            new LimitParameter(3),
            new DepthParameter(0),
            new OrderParameter("elements.post_date")
        );

        return View(response.Items);
    }
}

Although great, for example, projects, this can break things like the Single Responsibility Principle (SRP) or the Open Closed Principle. Joe does a great job of explaining why query logic (or any logic) in your controllers could be troublesome in his blog post, Visualizing Thin ASP.Net Controllers via SOLID Principles. Basically, there can be more than one reason for this code to change; you canā€™t test the controller or logic in isolation, it can lead to god methods, and the query isnā€™t reusable.

Kentico has done a great job and giving you all the tools you need to query their headless CMS, Kontent, including the heavy hitter, IDeliveryClient. This interface alone will give you enough power to do nearly everything you need to do to get data from Kontent. šŸ’Ŗ

In order to call Kenticoā€™s DeliveryClient from your controller, you need to install the Kontent Delivery SDK. Since this is functioning as our data access layer, we are going to create a Kontent. Repository layer that will have all of the dependencies needed to interact with the Kontent API map the results to domain objects and return them without any references to Kentico to the UI layer. Whenever you have an external API, you should consider an abstraction like this around the code with tight coupling to the API. This allows you to only change your repository layer when those external dependencies inevitably change, rather than changing every layer of your project. The UI layer shouldnā€™t care if the database layer changes underneath it!

The Common Reuse Principle states

Donā€™t force users of a component (dll in our case) to depend on things they donā€™t need.

I donā€™t want to have to install the entire Kontent Delivery SDK in every piece of my project!

Project Architecture

Project Dependency Diagram

Data flow diagram

You can see here that the Web, Infrastructure and Repository projects depend on Core, but do not depend on each other. Weā€™ll use the D of SOLID, Dependency Inversion Principle, to inject the concrete classes the Web, Infrastructure, and Repository layers need without adding dependencies to those projects by putting only the interfaces in core! šŸ¤ 

The most important rule of the Dependency Inversion Principle is:

Donā€™t refer to volatile concrete classes. Refer to abstract interfaces instead. This rule applies in all languages, whether statically or dynamically typed.Ā 

If you use a new keyword, chances are youā€™re probably violating this principle.

Project Data Flow Diagram

There are two ways data can flow through the layers of this application. For Create/Update/Read/Delete (CRUD) pass through style calls, the UI layer calls an IRepository interface directly (green arrows). If our application has additional business logic thatā€™s required, like an EmailService, we implement that business logic in our Infrastructure project. In these cases, the web layer calls an IService that resides in the Infrastructure project, which then can call IRepository data access layer if it needs to (blue arrows).

Although this project doesnā€™t have a traditional database, this architecture also works well for applications that do!

Project Overview

Demo.Web

This project is the .Net Core 3.1 web application.Ā 

It containsĀ 

Ā 

This project is also responsible for the dependency injection container configuration. However, I would consider moving this logic into a Mapping or Bootstrap layer because it requires references to all concrete classes in order to tell the application how to resolve the interfaces. Hereā€™s a snippet of the code required to setup dependency injection for Kontentā€™s IDeliveryClient. IDeliveryClient, CachedDeliveryClient, ProjectOptions, DeliveryOptions, ITypeProvider are all from Kentico and therefore require a Kentico dependency.

services.AddSingleton<IDeliveryClient>(
    serviceProvider => new CachedDeliveryClient(
        serviceProvider.GetRequiredService<IOptions<ProjectOptions>>(),
        serviceProvider.GetRequiredService<ICacheManager>(),
        DeliveryClientBuilder.WithOptions(_ => serviceProvider.GetRequiredService<IOptions<DeliveryOptions>>().Value)
            .WithTypeProvider(serviceProvider.GetRequiredService<ITypeProvider>())
            .WithContentLinkUrlResolver(new CustomContentLinkUrlResolver())
            .Build()
    )
);

Demo.Infrastructure

This project is the business logic layer that contains concrete implementations of services.

This layer should not depend on the Web or Repository layers. It can call the repository layers via IRepositories that are injected into it.

Demo.KenticoKontent.Repository

This project is a repository layer for Kontent. The purpose of this project is to abstract Kentico API calls and models into its own layer for use throughout the application.

This layer enables a software system to interact with external systems by receiving, storing, and providing data when requested.Ā 

The output of this layer are Core models designed with Domain Driven Design in mind, with no references to Kentico.Ā 

Responsibilities
  • Holding all the generated models from Kontent
  • Contain all the API calls and logic to query Kontent
  • Mapping the result of the API calls to Core models to return to consumers
Adding Additional Data Integrations

If you have additional data sources, add another repository layer. When we wanted to add Azure Search to our project, we created a Demo.Search.Repository. If a service needs information from both, call both repository layers and consolidate the result in a core model and return the core model to the Web layer.

Project Dependency Diagram

Demo.Core

This project is the core layer where the domain models or core models live. It also houses all interfaces for the project.

ResponsibilitiesĀ 
  • Core Models
  • Interfaces for Repositories and Services
  • Constants, Enums, Global Extensions, CustomAttributes, etc.

Demo.KenticoKontent.Mapping

The purpose of this project is to hold all of the automapping profiles, so the application knows how to map Kontent models to core models. We also started to migrate some of the dependency injection registration here but did not complete it.

ResponsibilitiesĀ 
  • Automapper mapping profiles
  • Automapper configuration
  • Default dependency injection configuration

Wrapping Up

In Part 2, weā€™ll look at the code associated with this architecture and trace a request from a controller down into the repository layer and back up to the UI. šŸ™Œ

  1. .Net Core 3.1 with Kontent.ai – Part 1: Clean Architecture Design
  2. .Net Core 3.1 with Kontent.ai – Part 2: Clean Architecture Implementation
  3. .Net Core 3.1 with Kontent.ai – Part 3: How to Route and Link to Content
  4. .Net Core 3.1 with Kontent.ai – Part 4: Ā Helpful Tips When Using Linked Items in Kentico Kontent

Subscribe to Our Blog

Stay up to date on what BizStream is doing and keep in the loop on the latest in marketing & technology.