.NET Core 3.1 with Kontent.ai – Part 2: Clean Architecture Implementation

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.

A Quick Review

In Part 1 of my series on .Net Core Kontent.ai websites, we covered a recommended SOLID architecture for .Net Core websites.

One of the goals of the architecture was to abstract all querying logic from the controller layers of the Kontent Boilerplate repo for ASP.NET Core MVC behind a repository layer so that they were isolated and our code didn’t depend on them. We’d then use Automapper to map the Kontent class to a clean core model based on Domain Driven Design, returning the core model to our UI layer.

If we needed additional business logic, we had an Infrastructure layer for any services like Email or Recaptcha.

Kontent.ai Automapper Example

In this segment, we’ll take a peek at what the code looks like to accomplish this. Let’s dive in!

"This is so exciting"

Code

The HomeController lives in the Web project. Notice there is very little logic in the controller. It does 3 things: call the homePageRepository interface, return a NotFound 404 if no home page domain models were returned from the repository and return the view with the homePage domain model.

After some debate, we chose to send core models out to the view layer rather than create a bunch of additional ViewModels in the Web layer to save on development time and mapping logic.  Our core models were simple, so we were ok with this.  When a view needs more than one model, we created a wrapper core model.  Each View displays core models.  

using Demo.Core.Abstractions.Repositories;
using Microsoft.AspNetCore.Mvc;
public class HomeController : BaseController
{

    private readonly IHomepageRepository homepageRepository;

    public HomeController(IHomepageRepository homepageRepository)
    {
        this.homepageRepository = homepageRepository;
    }

    public async Task<ActionResult> Index()
    {

        var result = await homepageRepository.GetHomepageAsync();
        if (result == null){ return NotFound(); }
        return this.View(result);
    }
}

Here’s some of that Dependency Inversion Principle at work, IHomepageRepository is injected, so the controller has dependencies on the interfaces in core instead of the class in the repository layer.

In your startup.cs or your bootstrapper project, you can setup auto registration so any interface with the same name as a concrete class will automatically get configured. You can also see the Automapper config below.

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    // ...

    DependencyConfig.Register(services);
}

Boostrapper

public static class DependencyConfig {
    public static void Register(IServiceCollection services)
    {
        AddRepositories(services);
        ConfigureAutomapper(services);
    } 

    private static void AddRepositories(IServiceCollection services)
    {        
        RegisterInterfaces("Repository", services, Assembly.GetAssembly(typeof(IHomepageRepository)), Assembly.GetAssembly(typeof(HomepageRepository)));
    } 

    private static void RegisterInterfaces(string interfaceType, IServiceCollection services, Assembly coreAssembly, Assembly serviceAssembly)
    {
        // Finds all classes who have a matching interface in the same
        // assemblies as IHomepageRepository and HomepageRepository
        var matches = serviceAssembly.GetTypes()
            .Where(t => t.Name.EndsWith(interfaceType, StringComparison.Ordinal) && t.GetInterfaces().Any(i => i.Assembly == coreAssembly))
            .Select(t => new {serviceType = t.GetInterfaces().FirstOrDefault(i => i.Assembly == coreAssembly), implementingType = t}).ToList();

        // Registers the interface to the implementation.
        foreach (var match in matches)
        {
            services.AddSingleton(match.serviceType, match.implementingType);
        }
    }

    private static void ConfigureAutomapper(IServiceCollection services)
    {
        // Automatically adds all AutoMapper profiles found in the Demo.Mapping namespace  
        var mappingConfig = new MapperConfiguration(cfg => cfg.AddMaps(new[] {"Demo.Mapping"}));
        IMapper mapper = mappingConfig.CreateMapper( );
        services.AddSingleton(mapper);
    }
}

All interfaces live in Core.

Core screenshot
using Demo.Core.Models.Homepage;
namespace Demo.Core.Abstractions.Repositories
{
    public interface IHomepageRepository
    {
        Task<Homepage> GetHomepageAsync();
    }
}

And its concrete implementation lives in Demo.KenticoKontent.Repository

Demo.KenticoKontent.Repository screenshot
using AutoMapper;
using KenticoCloud.Delivery;
using Demo.Core.Abstractions.Repositories;
using Demo.Core.Models.Homepage;
using Demo.KenticoCloud.Repository.Constants;
using KontentHomepage = Demo.KenticoCloud.Repository.ContentTypes.Homepage;

namespace Demo.KenticoCloud.Repository.Repositories
{
    public class HomepageRepository : IHomepageRepository
    {
        protected IDeliveryClient deliveryClient { get; }
        protected IMapper mapper { get; }

        public HomepageRepository(IDeliveryClient deliveryClient, IMapper mapper)
        {
            this.deliveryClient = deliveryClient;
            this.mapper = mapper;
        }

        public async Task<Homepage> GetHomepageAsync()
        {
            var response = await this.deliveryClient.GetItemsAsync<KontentHomepage>(
                new EqualsFilter("system.type", KontentHomepage.Codename),
                new OrderParameter("system.last_modified", SortOrder.Descending),
                new LimitParameter(1),
                new DepthParameter(2));

            var kontentHomepage = response.Items.FirstOrDefault();
            // Use autoMapper to map the kontent homepage to a core homepage
            return this.mapper.Map<Homepage>(kontentHomepage);
        }
    }
}

As long as the Homepage is already defined in Kontent, you can use the Kentico model generator to generate a C# class that the SDK will automatically serialize the JSON response from the API into. It looks something like this and lives in the repository layer. We decided to put this in the repository layer because it’s tightly coupled to the Kontent API call, and it has Kentico SDK dependencies.

using KenticoCloud.Delivery;
namespace Demo.KenticoCloud.Repository.ContentTypes
{
    public partial class Homepage
    {
        public const string Codename = "homepage";
        public const string SmallOverlayTitleCodename = "small_overlay_title";
        public const string MetadataOpenGraphImageCodename = "metadata__open_graph_image";
        // ...

        public string SmallOverlayTitle { get; set; }
        public IEnumerable<Asset> MetadataOpenGraphImage { get; set; }
        public ContentItemSystemAttributes System { get; set; }
        // ...
    }
}

The Kentico dependencies are almost hidden here. 🧐 Asset and ContentItemSystemAttributes are both classes provided by the Kontent SDK.

The domain model we created to return this data to the UI is Homepage.cs and it lives in the Core project under the Models namespace.

Homepage.cs screenshot
using Demo.Core.Models.General;
using Demo.Core.Models.Material;
namespace Demo.Core.Models.Homepage
{

    public class Homepage : BaseCoreModel
    {
        public string Codename { get; set; }
        public string SmallOverlayTitle { get; set; }
        // ...
    }
}

The mapping logic lives in Demo.Mapping

Demo.Mapping screenshot
using AutoMapper;
using Demo.Core.Models.General;
using Demo.KenticoCloud.Repository.ContentTypes;
namespace Demo.Mapping.Profiles
{

    public class HomepageMappingProfile : Profile
    {

        public HomepageMappingProfile()
        {
            //Create a mapping from Kontent's Homepage to our Homepage domain model
            CreateMap<Homepage, Core.Models.Homepage.Homepage>()
                //Example of mapping a string to a string
                .ForMember(dest => dest.Codename, opt => opt.MapFrom(src => src.System.Codename))
                // ...
        }
    }
}

Wrapping Up

Now you have a good idea of how to setup dependency injection to upload the Dependency Inversion Principle and how to segregate your code in the appropriate layers to enable easy maintenance in the future! 😻

In Part 3, we’ll look at how we can architect Kontent’s linked items to have our codebase automatically generate URLs and route to them based on what type of content is linked.

Be sure to check out the whole .Net Core 3.1 with Kontent.ai Series:

Subscribe to Our Blog

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