.Net Core 3.1 with Kentico Kontent Part 2: Clean Architecture Implementation
Be sure to check out the whole .Net Core 3.1 with Kentico Kontent Series:
  1. .Net Core 3.1 with Kentico Kontent Part 1: Clean Architecture Design 
  2. .Net Core 3.1 with Kentico Kontent Part 2: Clean Architecture Implementation
  3. .Net Core 3.1 with Kentico Kontent Part 3: How to Route and Link to Content
  4. .Net Core 3.1 with Kentico Kontent Part 4:  Helpful Tips When Using Linked Items in Kentico Kontent  (Coming soon)

A Quick Review

In Part 1 of my series on .Net Core Kentico Kontent 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 Kentico 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.

Kentico Kontent Automapper Example

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

"That's exciting" GIF

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 set upsetup 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. 

Questions? Comments? I’d love talking about architecture, just drop a comment!

Share This Post:

Twitter Pinterest Facebook Google+
Click here to read more Kentico posts
Start a Project with Us
Photo of the author, Joel Burke

About the author

Born in Ohio, Joel is an energetic programmer with over 12 years of Agile experience as a Full Stack Developer. He loves his brilliant wife Rebekah, documentation, communication, food, self-improvement, dressing up, doing things for people and learning. Incredibly passionate about faith, family, agile processes, people, software development and solving hard problems in each of these areas. Outside of work you’ll find him gaming with Rebekah, relaxing and enjoying great food.

View other posts by Joel

Subscribe to Updates

Stay up to date on what BizStream is doing and keep in the loop on the latest with Kentico.