dot CMS

How to Integrate .NET Applications With dotCMS

How to Integrate .NET Applications With dotCMS
Kevin

Kevin Davila

Senior Software Engineer

Share this article on:

The dotCMS Universal Visual Editor (UVE) is a framework-agnostic editor that enables full in-context editing for headless websites, single-page applications (SPAs), and other remote web applications using dotCMS as their content source.

In this guide, we'll walk through a simple step-by-step process to connect dotCMS services with a .NET project.


Prerequisites


Before integrating UVE into your .NET project, make sure your development environment is properly configured. You will need:

What We’re Building

We will begin by building a simple .NET App that’s fully editable with dotCMS Universal Visual Editor. Let's see the final result of the tutorial:

First Image.gif

dotCMS Page Layout Basics

dotCMS has a flexible page structure consisting of the following hierarchy:

  • Rows → Columns → Containers → Content (e.g., news, events, blog posts).

This structure enables easy content organization within dotCMS pages.

Example Diagram:

second image.png

Learn more about page structure here.


dotCMS Page API

The dotCMS Page API provides everything we need to build custom pages in a headless architecture. The response includes details about:

  • Layout

  • Containers

  • Page

This will be our main tool for retrieving data to display in our .NET project.


Getting your dotCMS Environment ready

Our Example Page

We'll use the existing "Home" page in the dotCMS demo instance as our example.

You can access it through UVE as follows:

  1. Browse to Site > Pages.

  2. Search Home

  3. Open the page

Third Image.gif

Building Our .NET project + dotCMS Page

First we will create a new .NET app using the CLI

dotnet new webapp -o MyDotCMSExample


With this, our project is now created. Let’s open it in our code editor, where we’ll see the different folders that make up the project structure. Navigate to the Pages folder and locate the Index.cshtml.cs and Index.cshtml files.

These are the files where we’ll write our code. Start by replacing the contents of Index.cshtml.cs with the following:

using System.Net.Http;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace MyDotCMSExample.Pages
{
    public class DotCMSPageModel : PageModel
    {
        private readonly ILogger<DotCMSPageModel> _logger;
        private readonly HttpClient _httpClient;
        private readonly IConfiguration _configuration;

        public DotCMSPageModel(ILogger<DotCMSPageModel> logger, IConfiguration configuration)
        {
            _logger = logger;
            _configuration = configuration;
            _httpClient = new HttpClient();
        }

        public PageData PageContent { get; set; }

        public async Task<IActionResult> OnGetAsync()
        {
            try
            {
                var TOKEN = "YOUR_TOKEN_HERE"
                    ?? throw new InvalidOperationException("Missing API token in configuration");

                var DOTCMS_INSTANCE_URL = "https://demo.dotcms.com";
                var PAGE_PATH = "/";

                var url = $"{DOTCMS_INSTANCE_URL}/api/v1/page/json{PAGE_PATH}";
                var request = new HttpRequestMessage(HttpMethod.Get, url);
                request.Headers.Add("Authorization", $"Bearer {TOKEN}");
                request.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));

                var response = await _httpClient.SendAsync(request);
                response.EnsureSuccessStatusCode();

                var jsonString = await response.Content.ReadAsStringAsync();
                var options = new JsonSerializerOptions
                {
                    PropertyNameCaseInsensitive = true,
                    AllowTrailingCommas = true,
                    ReadCommentHandling = JsonCommentHandling.Skip
                };

                var responseData = JsonSerializer.Deserialize<ApiResponse>(jsonString, options);
                PageContent = responseData.Entity;
                Console.WriteLine(jsonString);

                return Page();
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error loading page data");
                return StatusCode(500, "Error loading page: " + ex.Message);
            }
        }
    }

    public class ApiResponse
    {
        public PageData Entity { get; set; }
    }

    public class PageData
    {
        public PageInfo Page { get; set; }
        public LayoutData Layout { get; set; }
        public Dictionary<string, ContainerData> Containers { get; set; }
    }

    public class PageInfo
    {
        public string FriendlyName { get; set; }
    }

    public class LayoutData
    {
        public BodyData Body { get; set; }
    }

    public class BodyData
    {
        public List<RowData> Rows { get; set; }
    }

    public class RowData
    {
        public List<ColumnData> Columns { get; set; }
        public string StyleClass { get; set; }
    }

    public class ColumnData
    {
        public List<ContainerRef> Containers { get; set; }
        public int Left { get; set; }
        public int LeftOffset { get; set; }
        public bool Preview { get; set; }
        public string StyleClass { get; set; }
        public int Width { get; set; }
        public int WidthPercent { get; set; }
    }

    public class ContainerRef
    {
        public List<string> HistoryUUIDs { get; set; }
        public string Identifier { get; set; }
        public string UUID { get; set; }
    }

    public class ContainerData
    {
        public Container Container { get; set; }
        public List<ContainerStructure> ContainerStructures { get; set; }
        public Dictionary<string, List<Contentlet>> Contentlets { get; set; }
    }

    public class Container 
    {
        public int maxContentlets { get; set; }
        public string variantId { get; set; }
    }

    public class ContainerStructure
    {
        public string ContentTypeVar { get; set; }
    }

    public class Contentlet
    {
        public string Identifier { get; set; }
        public string BaseType { get; set; }
        public string ContentType { get; set; }
        public string Title { get; set; }
        public string WidgetTitle { get; set; }
        public string Inode { get; set; }
        public string Image { get; set; }
        public string Caption { get; set; }
        public string ButtonText { get; set; }
        public string Link { get; set; }
        public string RetailPrice { get; set; }
        public string SalePrice { get; set; }
        public string UrlTitle { get; set; }
        public string Description { get; set; }
    }
}

This defines the class and model for our page in dotCMS.

Next, update the following values within the code you just pasted:


Then, update the model reference in the Pages/Index.cshtml file so we can access the class directly from the HTML page.

@page
@model MyDotCMSExample.Pages.DotCMSPageModel

And then we can run the application with dotnet run and see the console.

As soon as you run the command above, it will indicate the port on which the page is hosted. Take a moment to check that URL out — something like http://localhost:5000, though the port may be slightly greater than 5000.

image four.png

We’re now successfully retrieving data from our page hosted in dotCMS — awesome!

Now that we have our page, we can render it in our HTML file. To do this, replace the contents of Pages/Index.cshtml with the following:

@page
@model MyDotCMSExample.Pages.DotCMSPageModel
@{
    ViewData["Title"] = Model.PageContent?.Page?.FriendlyName ?? "Page";
}

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ViewData["Title"]</title>
    <link rel="stylesheet" href="~/css/style.css" />
</head>
<body>
    <header class="container" style="background-color: rgba(0, 0, 0, 0.5); text-align: center;">
        <h1>@ViewData["Title"]</h1>
    </header>


    @if (Model.PageContent?.Layout?.Body?.Rows != null)
    {
        foreach (var row in Model.PageContent.Layout.Body.Rows)
        {
            <div class="container">
                <div data-dot-object="row" class="row @(row.StyleClass ?? "")">
                    @foreach (var column in row.Columns)
                    {
                        var startClass = $"col-start-{column.LeftOffset}";
                        var endClass = $"col-end-{column.Width + column.LeftOffset}";
                        
                        <div data-dot-object="column" class="@startClass @endClass @(column.StyleClass ?? "")">
                            @foreach (var containerRef in column.Containers)
                            {
                                var containerData = Model.PageContent.Containers[containerRef.Identifier];
                                var contentlets = containerData.Contentlets
                                    .GetValueOrDefault($"uuid-{containerRef.UUID}") 
                                    ?? containerData.Contentlets
                                    .GetValueOrDefault($"uuid-dotParser_{containerRef.UUID}");

                                if (contentlets != null)
                                {
                                    var acceptTypes = string.Join(",", containerData.ContainerStructures.Select(s => s.ContentTypeVar));
                                    
                                    <div data-dot-object="container"
                                         data-dot-identifier="@(containerData.Container?.GetType().GetProperty("Path")?.GetValue(containerData.Container) ?? containerRef.Identifier)"
                                         data-dot-accept-types="@acceptTypes"
                                         data-max-contentlets="@(containerData.Container?.GetType().GetProperty("maxContentlets")?.GetValue(containerData.Container))"
                                         data-dot-uuid="@containerRef.UUID">
                                        
                                        @foreach (var content in contentlets)
                                        {
                                            <div data-dot-object="contentlet"
                                                 data-dot-identifier="@content.Identifier"
                                                 data-dot-basetype="@content.BaseType"
                                                 data-dot-title="@(content.WidgetTitle ?? content.Title)"
                                                 data-dot-inode="@content.Inode"
                                                 data-dot-type="@content.ContentType"
                                                 data-dot-container='@Json.Serialize(new {
                                                     acceptTypes,
                                                     identifier = containerData.Container?.GetType().GetProperty("Path")?.GetValue(containerData.Container) ?? containerRef.Identifier,
                                                     maxContentlets = @containerData.Container.maxContentlets,
                                                     variantId = "DEFAULT",
                                                     uuid = containerRef.UUID
                                                 })'>


                                                
                                                @switch (content.ContentType)
                                                {
                                                    case "Banner":
                                                        <article>
                                                            @if (!string.IsNullOrEmpty(content.Image))
                                                            {
                                                                <img src="https://demo.dotcms.com/dA/@content.Identifier/image" alt="@content.Title">
                                                            }
                                                            @if (!string.IsNullOrEmpty(content.Title))
                                                            {
                                                                <h2>@content.Title</h2>
                                                            }
                                                            @if (!string.IsNullOrEmpty(content.Caption))
                                                            {
                                                                <p>@content.Caption</p>
                                                            }
                                                            @if (!string.IsNullOrEmpty(content.ButtonText))
                                                            {
                                                                <a href="@(content.Link ?? "#")">@content.ButtonText</a>
                                                            }
                                                        </article>
                                                        break;

                                                    case "Product":
                                                        <article>
                                                            @if (!string.IsNullOrEmpty(content.Image))
                                                            {
                                                                <img src="https://demo.dotcms.com/dA/@content.Identifier/image" alt="@content.Title">
                                                            }
                                                            <div>
                                                                <h3>@content.Title</h3>
                                                                <div>@content.RetailPrice</div>
                                                                <div>@content.SalePrice</div>
                                                                <a href="/store/products/@content.UrlTitle">Buy Now</a>
                                                            </div>
                                                        </article>
                                                        break;

                                                    case "Activity":
                                                        <article>
                                                            @if (!string.IsNullOrEmpty(content.Image))
                                                            {
                                                                <img src="https://demo.dotcms.com/dA/@content.Identifier/image" alt="@content.Title">
                                                            }
                                                            <div>
                                                                <h3>@content.Title</h3>
                                                                <p>@content.Description</p>
                                                                <a href="/activities/@content.UrlTitle">Link to detail →</a>
                                                            </div>
                                                        </article>
                                                        break;
                                                }
                                            </div>
                                        }
                                    </div>
                                }
                            }
                        </div>
                    }
                </div>
            </div>
        }
    }
</body>
</html>

This code takes the page object and iterates through each row, column, container, and contentlet (that is, a unit of content), rendering the content according to its type.

Additionally, we add some data-attributes to the wrapper divs of containers and contentlets. These attributes allow us to communicate with UVE when we want to edit our page. There's no need to go into detail about each one — simply adding the data-attributes with their corresponding values (taken from the page object) is enough for UVE to work properly.

It’s important to include all the required data-attributes with their correct values, so be careful when modifying them.

Now we can run our application and we should see something like this:

fifth image.png

Let’s replace the contents of wwwroot/css/site.css with the following:

/* Grid layout styles */
.row {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: 1rem;
  margin: 1rem 0;
}

/* Column styles */
[data-dot-object="column"] {
  grid-column: var(--start, auto) / var(--end, auto);
}

/* Article styles */
article {
  padding: 1rem;
  border: 1px solid #eee;
  border-radius: 4px;
}

article img {
  max-width: 100%;
  height: auto;
  margin-bottom: 1rem;
}

article h2, article h3 {
  margin: 0.5rem 0;
}

article p {
  margin: 0.5rem 0;
  color: #666;
}

article a {
  display: inline-block;
  margin-top: 1rem;
  padding: 0.5rem 1rem;
  background-color: #007bff;
  color: white;
  text-decoration: none;
  border-radius: 4px;
}

article a:hover {
  background-color: #0056b3;
} 

.row {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: 1rem;
}

.col-start-1 {
  grid-column-start: 1;
}

.col-start-2 {
  grid-column-start: 2;
}

.col-start-3 {
  grid-column-start: 3;
}

.col-start-4 {
  grid-column-start: 4;
}

.col-start-5 {
  grid-column-start: 5;
}

.col-start-6 {
  grid-column-start: 6;
}

.col-start-7 {
  grid-column-start: 7;
}

.col-start-8 {
  grid-column-start: 8;
}

.col-start-9 {
  grid-column-start: 9;
}

.col-start-10 {
  grid-column-start: 10;
}

.col-start-11 {
  grid-column-start: 11;
}

.col-start-12 {
  grid-column-start: 12;
}

.col-end-1 {
  grid-column-end: 1;
}

.col-end-2 {
  grid-column-end: 2;
}

.col-end-3 {
  grid-column-end: 3;
}

.col-end-4 {
  grid-column-end: 4;
}

.col-end-5 {
  grid-column-end: 5;
}

.col-end-6 {
  grid-column-end: 6;
}

.col-end-7 {
  grid-column-end: 7;
}

.col-end-8 {
  grid-column-end: 8;
}

.col-end-9 {
  grid-column-end: 9;
}

.col-end-10 {
  grid-column-end: 10;
}

.col-end-11 {
  grid-column-end: 11;
}

.col-end-12 {
  grid-column-end: 12;
}

.col-end-13 {
  grid-column-end: 13;
}

These styles take advantage of dotCMS’s grid system to properly position our content. The rest of the styling is up to your preference.

sixth image.png

So far, we’ve successfully rendered our page content in our .NET application — great job!

Let’s take it a step further and make this application editable directly from UVE.


To do that, we’ll download the dot-uve.js file, which contains everything needed to enable communication with UVE from external applications (built with any web technology). Place this file in the wwwroot/js directory and reference it in your Pages/Index.cshtml file with a script tag, you can use this file as a reference for how to do it.

If anything fails to update, terminate the dotnet process, and use dotnet run again.

There’s no need to dive into the contents of the JavaScript file; the key takeaway is that it handles the communication layer between the web application and UVE. However, one part worth highlighting is the following code block:

dotUVE.createSubscription('changes', () => {
    window.location.reload();
})

This function acts as a listener that waits for changes coming from UVE. So, whenever the application is opened inside UVE and any modifications are made, this listener will react and execute the provided callback.

For now, we’ll simply reload the entire window whenever there’s a change, allowing edits to be reflected instantly. Of course, you can implement a more specific refresh mechanism if needed.

For more details, feel free to explore the documentation for our UVE SDK.


Universal Visual Editor Configuration

Let’s start by configuring the UVE in dotCMS for our .NET application. This step is needed to use UVE to edit pages built in any JavaScript framework and served by any external hosting service. Next, let’s set up the Universal Visual Editor:

  1. Browse to Settings > Apps

  2. Select the built-in integration for UVE - Universal Visual Editor.

  3. Select the site that will be feeding the destination pages.

seventh image.png

In the configuration section, add your JSON object configuration. For this example, we’ll use a simple configuration to work with a localhost environment:

{
  "config": [
    {
      "pattern": ".*",
      "url": "http://localhost:REPLACE_WITH_YOUR_NET_APP_PORT"
    }
  ]
}

These properties specify the following information:

  • pattern: a regular expression (RegEx) that identifies to dotCMS which URLs should be associated with the .NET application. In this case, we’ve indicated all possible URLs.

  • url: the location of our .NET app

Note: If you want to know more about Universal Visual Editor configuration and all you can do with it, check out our documentation: Universal Visual Editor Configuration for Headless Pages.

Now, head over to your page inside UVE and try editing, adding, moving or deleting content:

eighth image.png

And that’s it 🎉! We’ve connected our .NET application with dotCMS and made it fully editable.


Conclusion and Next Steps

In this tutorial, we’ve successfully connected a .NET app to dotCMS, fetched page assets, rendered dynamic content, and made it editable! 🥳 🚀. 

For the next steps, consider exploring dotCMS Content Types or the Page API to suit your app's needs.