Xperience by Kentico's new Content Retrieval and Mapping API is powerful and flexible. But with all things powerful and flexible, there are some complexities, some differences from how things were done in the past, and some awesome hidden features.

Different scenarios require different approaches, so let's get into how to best leverage the API to accomplish your goals.

This post was written for Xperience by Kentico v30.1.0. Please consult the documentation for any feature or API changes when using a different version. Be sure to check the System Requirements in the product documentation.

Content Item and Reusable Schema Retrieval

With anything concerning the Content Item system you're going to need to use the ContentItemQueryBuilder to retrieve the content. This has the systems in place to handle versioning, proper language fallbacks, and linked items.

You'll also be leveraging the IContentQueryExecutor and its Results and/or MappedResults methods.

Scenario 1: Retrieve a single Content Item Type + Linked Items

The first scenario is probably the most common, you need to get your Content Items and the linked items associated with them.

var getBasicPages = new ContentItemQueryBuilder()
    .ForContentType(BasicPage.CONTENT_TYPE_NAME,
        query => query
          // o=> o.IncludeWebPageData() if items may be WebPageItems and you need the data.
          .WithLinkedItems(2, o => o.IncludeWebPageData())
    )
    .InLanguage(_preferredLanguageRetriever.Get());

var mappedToPage = await _contentQueryExecutor
    .GetMappedWebPageResult<BasicPage>(
        getBasicPages,
        new ContentQueryExecutionOptions()
        {
            ForPreview = _previewFeature.Enabled
        });

if (mappedToPage.Any())
{
    var firstItem = mappedToPage.First();
    var mappedToPageResult = $"{firstItem.SystemFields.WebPageItemName} - {firstItem.MetaData_Title}";
}

The WithLinkedItems will make sure that you retrieve additional items in the model, and the GetMappedWebPageResult will automatically type cast and include your data from your page.

Scenario 2: Retrieve Reusable Schema Data

Sometimes you don't know what specific content type you want, but you know you want the data found in a Reusable Schema. An example may be if you have an IBaseMetadata that has the Title and description fields you want for a listing page of multiple content items. For this, you can use the GetMappedResult<IYourReusableSchema>

var getMetaData = new ContentItemQueryBuilder()
    .ForContentTypes(
        query => query
            .OfReusableSchema([IBaseMetadata.REUSABLE_FIELD_SCHEMA_NAME])
            // o => o.IncludeWebPageData() if items may be WebPageItems and you need the data.
            .WithLinkedItems(2, o => o.IncludeWebPageData())
    )
    .InLanguage(_preferredLanguageRetriever.Get());

var mappedToReusableSchemaInterface = await _contentQueryExecutor
    .GetMappedResult<IBaseMetadata>(
        getMetaData,
        new ContentQueryExecutionOptions()
        {
            ForPreview = _previewFeature.Enabled
        });

if (mappedToReusableSchemaInterface.Any())
{
    var firstItem = mappedToReusableSchemaInterface.First();
    var mappedMetaDataResults = $"{firstItem.MetaData_Title}";
}

Scenario 3: Retrieve Reusable Schema Data + Check for Additional Model Types

Keep in mind that the Xperience model Mapper will still map each object to the actual registered C# Model created through the --kpx-codegen scripts, even if you only are mapping to a Reusable Schema interface. Because of this, you can do additional type checking and casting to retrieve additional fields.

In the below example we are retrieving any content item that uses our IBaseMetadata Reusable Schema and retrieving its OG Image (which is of type IGenericHasImage, an empty interface that just indicates it has an image). The OG Image can be any content type that has an image and inherits IGenericHasImage so we need to do some type casting with it.

var getMetaDataWithOtherData = new ContentItemQueryBuilder()
    .ForContentTypes(
        query => query
            .OfReusableSchema([IBaseMetadata.REUSABLE_FIELD_SCHEMA_NAME])
            .WithWebPageData()
            // o=> o.IncludeWebPageData() if items may be WebPageItems and you need the data.
            .WithLinkedItems(2, o => o.IncludeWebPageData())
    )
    .InLanguage(_preferredLanguageRetriever.Get());

var mappedToReusableSchemaInterfaceThatInheritsFromIWebPageFieldSource = await _contentQueryExecutor
    .GetMappedWebPageResult<IBaseMetadata>(
        getMetaDataWithOtherData,
        new ContentQueryExecutionOptions()
        {
            ForPreview = _previewFeature.Enabled
        });
        
if (mappedToReusableSchemaInterfaceThatInheritsFromIWebPageFieldSource.Any())
{
    var firstItem = mappedToReusableSchemaInterfaceThatInheritsFromIWebPageFieldSource.First();
    var mappedMetaDataResults = $"{firstItem.MetaData_Title}";
    // Use type casting to expose SystemFields
    if (firstItem is IWebPageFieldsSource webPageData)
    {
        mappedMetaDataResults += $" - {webPageData.SystemFields.WebPageItemTreePath}";
    }

    // MetaData_OGImage is an empty interface, no fields, but should be assigned to Image Types
    if (firstItem.MetaData_OGImage.Any())
    {
        // Use type checking to get data
        if (firstItem.MetaData_OGImage.First() is IContentItemFieldsSource contentItemFieldsSource)
        {
            mappedMetaDataResults += $" - {contentItemFieldsSource.SystemFields.ContentItemGUID}";
        }
        if (firstItem.MetaData_OGImage.First() is Generic.Image image)
        {
            mappedMetaDataResults += $" - {image.ImageTitle}";
        }
    }
}

Additionally, although this is a little hacky, you can have your Reusable Schema inherit IContentItemFieldSource or IWebPageFieldSource to expose those fields if applicable.

if (mappedToReusableSchemaInterfaceThatInheritsFromIWebPageFieldSource.Any())
{
    var firstItem = mappedToReusableSchemaInterfaceThatInheritsFromIWebPageFieldSource.First();
    // MetaData_OGImage Inherits from IContentItemFieldSource
    if (firstItem.MetaData_OGImage.Any())
    {
        var preMappedResult = $"{firstItem.MetaData_OGImage.First().SystemFields.ContentItemGUID}";
    }
}

Section System Example

A common web design pattern for building out pages is the Section Model, where a section is a horizontal portion of a page, with strongly typed models (ex: Hero Banner Section, Card Section, etc.), and your page contains multiple of these sections.

You would have an Interface (ISectionSystem) that contains a Content Items and Reusable Content selector field, with defined sections Content Types allowed, and from there the user can create/modify/remove/arrange the sections selected.

The Field will be rendered as the following on your model.

public IEnumerable<IContentItemFieldsSource> SectionsSystemSections { get; set; }

However, the mapper will map each section to its appropriate model (ex: HeroBannerSection, CardSection).

Here's the code:

var testPage = new ContentItemQueryBuilder()
    .ForContentType(Testing.WebPage.CONTENT_TYPE_NAME,
        query => query
            .WithLinkedItems(100, o => o.IncludeWebPageData())
            .ForWebsite(_websiteChannelContext.WebsiteChannelName, includeUrlPath: true)
            .Where(where => where.WhereEquals(nameof(WebPageFields.WebPageItemID), currentWebPageItemID))
            .TopN(1)
    )
    .InLanguage(_preferredLanguageRetriever.Get());

var mappedToPage = await _contentQueryExecutor
    .GetMappedWebPageResult<Testing.WebPage>(
        testPage,
        new ContentQueryExecutionOptions()
        {
            ForPreview = _previewFeature.Enabled
        });

if (mappedToPage.Any())
{
    var firstItem = mappedToPage.First();
    foreach (var section in firstItem.SectionsSystemSections)
    {
        if (section is Section.TestA testA)
        {
            var testing = testA.SectionTestAName;
        }
        else if (section is Section.TestB testB)
        {
            var testing = testB.SectionTestBName;
        }
        else if (section is Section.Widget widget)
        {
            // If you don't see the WebPageItem values, 
            // make sure your .WithLinkedItems has the optional 
            // ConfigureOptionsAction (query.WithLinkedItems(100, o=> o.IncludeWebPageData()))
            // Partial Widget Page 
            //   <inlinewidgetpage web-page-id="@webPageID" initialize-document-prior="true"></inlinewidgetpage>
            // requires the WebPageItemID
            var testing = widget.SystemFields.WebPageItemID;
        }
    }
}

Object / Data Retrieval

If you are retrieving custom module data, there are multiple ways to accomplish this. The primary method will be to use the IInfoProvider<TInfoClass> interface, but additionally sometimes you will want to run a straight query (using the DataQuery 's CustomQueryText) and then map.

Warning: Running queries directly against the database, while faster as it skips any of the API logic, also can lead to un-detectable errors.

Make sure use direct queries responsibly, with nameof(MyInfoType.MyFieldName) so removing fields will result in compile time errors you can detect and correct, as well as make sure to level SQL query parameters instead of direct-injected or string interpolated input if anything is user-driven.

Scenario 1: Retrieve data from one source (not retrieving joined data)

The first scenario is simply retrieving data of a single AbstractInfo<> model. You can still use the Source method to join for filtering, but you don't need any data except the primary.

// _mediaFileInfoProvider is IInfoProvider<MediaFileInfo> type
var itemsFromOneSource = await _mediaFileInfoProvider.Get()
    .Source(
        x => x.InnerJoin<MediaLibraryInfo>(
            nameof(MediaFileInfo.FileLibraryID),
            nameof(MediaLibraryInfo.LibraryID)))
    .WhereEquals(nameof(MediaLibraryInfo.LibraryName), "MyLibrary")
    .Columns(nameof(MediaFileInfo.FileName))
    .GetEnumerableTypedResultAsync();

if (itemsFromOneSource.Any())
{
    var firstItem = itemsFromOneSource.First();
    var testOneSource = $"{firstItem.FileName}";
}

Scenario 2: Retrieve data from multiple sources

A major departure from previous versions of Kentico is that mapped Info Classes no longer contain any joined fields of other types. To overcome this, you'll have to return the IDataContainer via the GetDataContainerResultAsync instead of the GetEnumeratedTypedResultAsync method.

The IDataContainer is the equivalent of a DataRow, and contains the raw data (including join data). You can then use the _____Info.New(myIDataContainer) to map to multiple sources.

var itemsFromTwoSources = await _mediaFileInfoProvider
    .Get()
    .Source(
        x => x.InnerJoin<MediaLibraryInfo>(
            nameof(MediaFileInfo.FileLibraryID),
            nameof(MediaLibraryInfo.LibraryID)))
    .Columns(
        nameof(MediaFileInfo.FileName),
        nameof(MediaLibraryInfo.LibraryDisplayName))
    .GetDataContainerResultAsync();

var parsedItemsFromTwoSources = itemsFromTwoSources
    .Select(x => (mediaFile: MediaFileInfo.New(x), mediaLibrary: MediaLibraryInfo.New(x)));

if (parsedItemsFromTwoSources.Any())
{
    var firstItem = parsedItemsFromTwoSources.First();
    var testTwoSource = $"{firstItem.mediaFile.FileName} - {firstItem.mediaLibrary.LibraryDisplayName}";
}

Scenario 3: A custom Query (single table)

If you need to run a complex query, or something that returns values you can't easily map, you can leverage the DataQuery along with the CustomQueryText property (and optional parameters), and use the GetDataContainerResultAsync to retrieve your rows. From here you can leverage the GetValue or TryGetValue methods on the IDataContainer results to get what you need:

string queryText = $"""
    select 
        Count(*) over (partition by {nameof(MediaFileInfo.FileExtension)}) as TotalOfThisFileType, 
        * from Media_File
    """;

var customQuery = new DataQuery
{
    CustomQueryText = queryText
};

var dataContainers = await customQuery.GetDataContainerResultAsync();

var parsedItemsFromDataQuery = dataContainers
    .Select(x => (MediaFile: MediaFileInfo.New(x), TotalOfType: (int)x.GetValue("TotalOfThisFileType")));

if (parsedItemsFromDataQuery.Any())
{
    var firstItem = parsedItemsFromDataQuery.First();
    var testsFromQuery = $"{firstItem.MediaFile.FileName} - {firstItem.TotalOfType}";
}

Querying From Different Database: Keep in mind that the DataQuery has many properties, including the connection string name. So, if you need to run a query against another database all together, you'll want to leverage this class with that ConnectionStringName property!

Scenario 4: A custom query (multiple tables)

The DataQuery class is the go-to in almost all cases for running straight queries, except if you wish to return multiple tables in a dataset. From here, you will need to go back to the old ConnectionHelper, which can be used to get your multiple datasets. From here you can still use the DataRow to map to fields or get values.

string queryText = $"""
    select 
        Count(*) over (partition by {nameof(MediaFileInfo.FileExtension)}) as TotalOfThisFileType, 
        * from Media_File

    select * from Media_Library
    """;

DbDataReader dataReader = await ConnectionHelper
    .ExecuteReaderAsync(
        queryText, 
        parameters: [], 
        queryType: QueryTypeEnum.SQLQuery, 
        commandBehavior: CommandBehavior.Default, 
        cancellationToken: default);

var dataSet = dataReader.ToDataSet();

var parsedMediaItemsFromQuery = dataSet
    .Tables[0]
    .Rows
    .Cast<DataRow>()
    .Select(x => (MediaFile: MediaFileInfo.New(x), TotalOfType: x.Field<int>("TotalOfThisFileType")));

if (parsedMediaItemsFromQuery.Any())
{
    var firstItem = parsedMediaItemsFromQuery.First();
    var testsFromQuery = $"{firstItem.MediaFile.FileName} - {firstItem.TotalOfType}";
}

var parsedLibraryItemsFromQuery = dataSet
    .Tables[1]
    .Rows
    .Cast<DataRow>()
    .Select(x => MediaLibraryInfo.New(x));

if (parsedLibraryItemsFromQuery.Any())
{
    var firstItem = parsedLibraryItemsFromQuery.First();
    var testsFromQuery = $"{firstItem.LibraryName}";
}

Here's the ToDataSet extension I call above on DbDataReader.

public static DataSet ToDataSet(this DbDataReader reader)
{
    var ds = new DataSet();
    if (reader is null)
    {
        ds.Tables.Add(new DataTable());
        return ds;
    }

    // read each data result into a datatable
    do
    {
        var table = new DataTable();
        table.Load(reader);
        ds.Tables.Add(table);
    } while (!reader.IsClosed);

    return ds;
}

Conclusion

I hope this little guide helps you in your data retrieval and modeling. Special thanks to Kentico's Principal Software Engineer, Jakub Oczko, who helped field a bunch of questions and these scenarios.