Mit dem Oktober Refresh 27.0.0 wurde in Xperience by Kentico die grundlegende Architektur umgebaut.
Statt mit TreeNodes und DocumentTypes haben wir es hier ab sofort mit Content Items und deren Sonderform Pages zu tun.
Die DocumentQuery und die MultiDocumentQuery sind entsprechend entfernt worden.

Wie fragen wir also ab jetzt unsere Page Daten aus der Datenbank ab?

Xperience by Kentico Content Item Query Struktur
© by Kentico Software GmbH ↗

Was war – Was ist

Wenn wir uns auf die Pages und die Baumstruktur in der Pages App konzentrieren, dann kann man die entsprechende neue Struktur ungefähr so interpretieren:

Vor XbyK 27.0.0Ab XbyK 27.0.0
TreeNode (Page Content Items)IWebPageFieldsSource
TreeNode Eigenschaften[MyContentTypeClass].SystemFields
DocumentQueryContentTypeQueryParameters
MultiDocumentQueryContentItemQueryBuilder

Content Items

Content Items können jetzt auch im Content Hub angelegt werden. Hier legen wir alles an, was nicht in direktem Zusammenhang mit einem konkreten Webseiten Kanal steht und in verschiedenen Kanälen genutzt werden kann (muss nicht zwingend eine Webseite sein).

Die Content Items haben daher sinnigerweise keinen Bezug zu einer Webseitenstruktur mehr.
Sie nutzen statt dessen definierbare Beziehungen zueinander.
Daher entfallen diese Page Eigenschaften und wir brauchen nur die grundlegenden Content Item Eigenschaften:

Diese Aufspaltung des TreeNodes in diese zwei Teilbereiche macht vor dem neuen Hintergrund durchaus Sinn.

Pages

Eine Page ist ein Content Item welches in der Pages App angelegt wird und von einem User besucht werden kann (Eine URL hat).
Eine Page liegt in einer Baumstruktur in Parent-Child Beziehung zu anderen Pages im Content Tree vor.

Schauen wir uns den automatisch generierten Code eines Content Types vom Typ „Page“ einmal an, dann fällt folgendes auf:

/// <summary>
/// Represents a page of type <see cref="Homepage"/>.
/// </summary>
public partial class Homepage : IWebPageFieldsSource
{
	/// <summary>
	/// Code name of the content type.
	/// </summary>
	public const string CONTENT_TYPE_NAME = "Xel.Homepage";

	/// <summary>
	/// Represents system properties for a web page item.
	/// </summary>
	public WebPageFields SystemFields { get; set; }

Vorher war dies:

/// <summary>
/// Represents a content item of type Homepage.
/// </summary>
public partial class Homepage : TreeNode
{
	#region "Constants and variables"

	/// <summary>
	/// The name of the data class.
	/// </summary>
	public const string CLASS_NAME = "Xel.Homepage";

Da jetzt alles ein Content Item ist und nicht mehr ausschließlich in einer Baumstruktur angelegt wird macht die Änderung hier Sinn.
Statt einem TreeNode haben wir also ein Content Item, welches zusätzlich das Interface IWebPageFieldsSource implementiert.
Dadurch wird das Content Item mit allen Feldern erweitert, die im Zusammenhang mit der Baumstruktur aus der Pages App stehen.
Das Interface stellt das WebPageFields Objekt „SystemFields“ sicher.

Konkrete Pages abfragen

Wir fangen erstmal mit einem QueryBuilder an.
Als Teil der Abrage (Hauptabfrage) filtern wir dann zuerst erstmal nach dem Typ den wir abfragen wollen. Das können auch mehrere sein.
Danach können wir für diese Typen noch weitere Details in einer untergeordneten Abfrage ergänzen (Unterabfrage).
Globale Parameter werden zuletzt zu dem ganzen Paket aus abgefragten Typen und deren Details hinzugefügt.

Um die Abfrage am Ende auszuführen nutzen wir einen QueryExecutor und mappen das Ergebnis dann direkt auf ein passendes Objekt.

Dafür gibt es zwei Hauptmethoden am Executor. Einmal GetResult() und einmal GetWebPageResult() für die Sondervariante „Page“ Content Item.

Das ist erstmal eine Umgewöhnung, aber je mehr man sich damit befasst umso mehr Vorteile werden durch diesen neuen Ansatz klar.

string pagePath = "[PATH TO YOUR PAGE IN THE PAGE TREE]";
IWebsiteChannelContext siteContext 
    = Service.Resolve<IWebsiteChannelContext>();
IContentQueryExecutor queryExecutor 
    = Service.Resolve<IContentQueryExecutor>();

// get current page context
WebPageDataContext pageContext 
    = pageContextRetriever.Retrieve();

// query builder to retrieve pageFilter in path
ContentItemQueryBuilder pageFilterBuilder 
    = new ContentItemQueryBuilder()
    .ForContentType(
        nameof(DocumentFilter.CONTENT_TYPE_NAME), 
        query => query
            .ForWebsite(
                siteContext.WebsiteChannelName, 
                PathMatch.Single(pagePath)))
    .InLanguage(pageContext.WebPage.LanguageName);

// get the DocumentFilter page
DocumentFilter documentFilter 
    = (await queryExecutor
        .GetWebPageResult(
            builder: pageFilterBuilder, 
            resultSelector: result 
                => resultMapper.Map<DocumentFilter>(result))
        )
    .FirstOrDefault();

Verschiedene Pages abfragen

Etwas komplexer ist die Abfrage wenn man mehrere Typen zusammen abfragen möchte.

Zu diesem Zweck können wir mehrere .ForWebsite subqueries an einen Builder anhängen.

Auf diese Weise gibt uns der Executor dann verschiedene Typen zurück.

Das Mapping des Results auf einen Datentyp muss dann entsprechend berücksichtigt werden.

In diesem Beispiel möchte ich gerne alle Pages abfragen, in denen einige Properties gleich sind (Vererbung kommt im Frühjahr 2024).
Aus dem Ergebnis der Abfrage möchte ich dann auf die Werte in diesen gemeinsamen Eigenschaften zugreifen.

foreach(string contentTypeName in BasePageInheritorClassnames)
{
    builder = builder
        .ForContentType(contentTypeName, query => query
            .ForWebsite(
                siteContext.WebsiteChannelName, 
                PathMatch.Children(pagePath, nestingLevel)));
}

IEnumerable<BasePage> pages = await queryExecutor.GetWebPageResult(
    builder: builder, 
    resultSelector: result => resultMapper.Map<BasePage>(result));

Fazit

Die neue Architektur und das Umdenken von reinen Webseiten hin zu modernen Content Beziehungen und Ausgaben auf mehreren Kanälen ist eine gewaltige Umgewöhnung insbesondere für die Entwickler (zu denen ich mich auch zähle), die seit Dekaden mit den DocumentQueries und den TreeNodes arbeiten.

Der Schritt war jedoch notwendig und bietet viel mehr Möglichkeiten um den Content aus der Xperience by Kentico ↗ DXP abzufragen.

Ich würde raten ein Repository Layer einzurichten um die Abfragen zentral zu verwalten und in Zukunft an einer Stelle anpassen zu können sobald mehr Features in XbyK implementiert werden die dies erlauben.

Quellen