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! 😬
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.
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:
But as soon as one unforeseen force hits you, everything crumbles around you and it ends up being more like this:
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.
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.
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!
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.
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!
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()
)
);
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.
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.
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.
This project is the core layer where the domain models or core models live. It also houses all interfaces for the project.
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.
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. 🙌
We love to make cool things with cool people. Have a project you’d like to collaborate on? Let’s chat!
Stay up to date on what BizStream is doing and keep in the loop on the latest in marketing & technology.