Remodel page types as reusable field schemas

The Xperience by Kentico: Kentico migration tool allows you to remodel your content on the fly during the upgrade process.

Using a custom class mapping, you can discard fields, rename them, transform their values, and create reusable field schemas that you can share among multiple classes.

This guide will extract common fields from two page types from Kentico Xperience 13 (KX13) and move them to a Reusable field schema shared by both web page content types in Xperience by Kentico (XbyK).

Set up the project

If you haven’t, we recommend following along with the upgrade walkthrough to see the process of getting a migrated page working in Xperience by Kentico.

In preparation to follow along with this guide, you need:

  • A running instance of Kentico Xperience 13 with the Dancing Goat template
  • An instance of the migration tool
  • A compatible install of Xperience by Kentico

If you haven’t followed along with the walkthrough, complete the steps from its environment setup page in preparation for this guide.

Determine the new model

As discussed in the migration strategy planning guide, page types in KX13 map to content types in XbyK.

The migration tool automatically converts page types to content types with the same fields, and converts their pages web page items in the content tree of a website channel. However, you can extend the process for specific page types, making structural adjustments as the tool converts them into content types, or changing where and how they are stored.

Examine the source classes

In the KX13 Dancing Goat site, there are two page types corresponding to coffee grinders: Manual grinder and Electric grinder.

The Manual grinder page type has the following fields:

  • ManualGrinderID
  • ManualGrinderPromotionTitle
  • ManualGrinderPromotionDescription
  • ManualGrinderBannerText

These are the Electric grinder fields:

  • ElectricGrinderID
  • ElectricGrinderPower
  • ElectricGrinderPromotionTitle
  • ElectricGrinderPromotionDescription
  • ElectricGrinderBannerText

Consolidate the common fields

As both page types contain fields for a promotion title, promotion description, and banner text, we can move these fields to a shared schema.

Each content type in XbyK will need its own ID, and the electric grinder content type will need a power field that is not part of the schema.

Diagram of grinder reusable field schema

Implement the class mapping

Add a mapping class

In your local version of the migration tool repository, add a new file to the ClassMappings folder of the Migration.Tool.Extensions project.

C#
GrinderClassMapping.cs

using CMS.DataEngine;
using CMS.FormEngine;
using Microsoft.Extensions.DependencyInjection;
using Migration.Tool.Common.Builders;
using Migration.Tool.KXP.Api.Auxiliary;

namespace Migration.Tool.Extensions.ClassMappings;

public static class GrinderClassMapping
{

}
...

Define string constants for names

Looking over the class mapping sample from the migration tool, you can see that the names of classes and fields are often used multiple times. For example, a schema’s name is used when defining the schema and when assigning it to a content type.

For the sake of keeping everything in one place, let’s use constants for the string values we’ll need in this class, even if they are only used once.

This example uses unusual casing in the constant names for the sake of legibility, but you can adjust it to your preferences.

C#
GrinderClassMapping.cs

...
public static class GrinderClassMapping
{
    // Source class names
    private const string Source_ClassName_Electric = "DancingGoatCore.ElectricGrinder";
    private const string Source_ClassName_Manual = "DancingGoatCore.ManualGrinder";

    // Source class field names
    private const string Source_FieldName_Electric_PromotionTitle = "ElectricGrinderPromotionTitle";
    private const string Source_FieldName_Electric_PromotionDescription = "ElectricGrinderPromotionDescription";
    private const string Source_FieldName_Electric_BannerText = "ElectricGrinderBannerText";
    private const string Source_FieldName_Electric_Power = "ElectricGrinderPower";
    private const string Source_FieldName_Manual_PromotionTitle = "ManualGrinderPromotionTitle";
    private const string Source_FieldName_Manual_PromotionDescription = "ManualGrinderPromotionDescription";
    private const string Source_FieldName_Manual_BannerText = "ManualGrinderBannerText";

    // Target class names
    private const string Target_ClassName_Schema = "DancingGoat.Grinder";
    private const string Target_ClassName_Electric = "DancingGoat.ElectricGrinder";
    private const string Target_ClassName_Manual = "DancingGoat.ManualGrinder";

    // Target class display names
    private const string Target_DisplayName_Schema = "Common Grinder Fields";
    private const string Target_DisplayName_Electric = "Electric Grinder";
    private const string Target_DisplayName_Manual = "Manual Grinder";

    // Target class schema descriptions
    private const string Target_Description_Schema = "Reusable schema that defines common grinder fields";

    // Target class table names
    private const string Target_TableName_Electric = "DancingGoat_ElectricGrinder";
    private const string Target_TableName_Manual = "DancingGoat_ManualGrinder";

    // Target class field names
    private const string Target_FieldName_Schema_PromotionTitle = "GrinderPromotionTitle";
    private const string Target_FieldName_Schema_PromotionDescription = "GrinderPromotionDescription";
    private const string Target_FieldName_Schema_BannerText = "GrinderBannerText";
    private const string Target_FieldName_Electric_Power = "ElectricGrinderPower";
    private const string Target_FieldName_Electric_ID = "ElectricGrinderID";
    private const string Target_FieldName_Manual_ID = "ManualGrinderID";

    // Target class field display names
    private const string Target_FieldDisplayName_Schema_PromotionTitle = "Promotion title";
    private const string Target_FieldDisplayName_Schema_PromotionDescription = "Promotion description";
    private const string Target_FieldDisplayName_Schema_BannerText = "Banner text";

    // Target class field GUIDs
    private const string Target_FieldGuid_Schema_PromotionTitle = "8E6C956F-FD45-42B3-A675-A7D39069A57B";
    private const string Target_FieldGuid_Schema_PromotionDescription = "8AC6414E-0AA5-40D8-830E-2B090F0C4723";
    private const string Target_FieldGuid_Schema_BannerText = "1804B982-59AB-459D-806E-AC55863116FE";

    // Setting names
    private const string Target_Setting_Schema_ControlName = "controlname";

    ...

With these constants in place, we can begin to define the classes and mappings.

Create the schema

Let’s start with the reusable field schema that will hold data common to both electric and manual grinders.

Schema fields defined during the migration can copy their details from fields in the source instance using the CreateFrom method.

In this case, let’s define the fields manually so we can set custom names for them, rather than taking the names from the source page type in KX13.

C#
GrinderClassMapping.cs

// Build schema for shared grinder fields
private static ReusableSchemaBuilder BuildSchema()
{
    var schemaBuilder = new ReusableSchemaBuilder(Target_ClassName_Schema, Target_DisplayName_Schema, Target_Description_Schema);

    schemaBuilder
        .BuildField(Target_FieldName_Schema_PromotionTitle)
        .WithFactory(() => new FormFieldInfo
        {
            Name = Target_FieldName_Schema_PromotionTitle,
            Caption = Target_FieldDisplayName_Schema_PromotionTitle,
            Guid = new Guid(Target_FieldGuid_Schema_PromotionTitle),
            DataType = FieldDataType.Text,
            Size = 200,
            Settings =
            {
                [Target_Setting_Schema_ControlName] = FormComponents.AdminTextInputComponent
            }
        });
    schemaBuilder
        .BuildField(Target_FieldName_Schema_PromotionDescription)
        .WithFactory(() => new FormFieldInfo
        {
            Name = Target_FieldName_Schema_PromotionDescription,
            Caption = Target_FieldDisplayName_Schema_PromotionDescription,
            Guid = new Guid(Target_FieldGuid_Schema_PromotionDescription),
            DataType = FieldDataType.Text,
            Size = 200,
            Settings =
            {
                [Target_Setting_Schema_ControlName] = FormComponents.AdminTextInputComponent
            }
        });
    schemaBuilder
        .BuildField(Target_FieldName_Schema_BannerText)
        .WithFactory(() => new FormFieldInfo
        {
            Name = Target_FieldName_Schema_BannerText,
            Caption = Target_FieldDisplayName_Schema_BannerText,
            Guid = new Guid(Target_FieldGuid_Schema_BannerText),
            DataType = FieldDataType.Text,
            Size = 200,
            Settings =
            {
                [Target_Setting_Schema_ControlName] = FormComponents.AdminTextInputComponent
            }
        });

    return schemaBuilder;
}

Map the electric grinder content type

Next, let’s move on to the Electric grinder content type.

Start by defining the essential class attributes corresponding to fields in the CMS_Class table, like the name, display name, and table name.

Add a primary key field and assign the reusable field schema to the content type.

Next, assign values to the corresponding schema fields.

Finally, create a new field for the grinder’s power using the KX13 field as a template.

Use KX13 fields as templates

The Kentico migration tool allows you to define content type fields on the fly as you map them.

For the schema fields used by the Electric grinder content type, we defined their attributes manually in the schema builder, then simply assigned them to the Electric grinder content type as we mapped data from KX13. However, we still need to define attributes of the power field.

Some migration tool methods, such as SetFrom, include an optional isTemplate parameter. The parameter lets you specify if the tool should replicate the KX13 field definition for the XbyK content type.

If isTemplate is set to true, the tool will transfer KX13 field information like the caption, data type, and default value to the XbyK content type. Otherwise, it will only transfer values.

See the class mapping sample file for more examples.

C#
GrinderClassMapping.cs

...
// Migrate electric grinders
private static MultiClassMapping BuildElectricGrinderMapping()
{
    var mappingElectric = new MultiClassMapping(Target_ClassName_Electric, target =>
    {
        target.ClassName = Target_ClassName_Electric;
        target.ClassTableName = Target_TableName_Electric;
        target.ClassDisplayName = Target_DisplayName_Electric;
        target.ClassType = ClassType.CONTENT_TYPE;
        target.ClassContentTypeType = ClassContentTypeType.WEBSITE;
    });

    mappingElectric.BuildField(Target_FieldName_Electric_ID).AsPrimaryKey();

    mappingElectric.UseResusableSchema(Target_ClassName_Schema);

    mappingElectric.BuildField(Target_FieldName_Schema_PromotionTitle)
        .SetFrom(Source_ClassName_Electric, Source_FieldName_Electric_PromotionTitle);

    mappingElectric.BuildField(Target_FieldName_Schema_PromotionDescription)
        .SetFrom(Source_ClassName_Electric, Source_FieldName_Electric_PromotionDescription);

    mappingElectric.BuildField(Target_FieldName_Schema_BannerText)
        .SetFrom(Source_ClassName_Electric, Source_FieldName_Electric_BannerText);

    mappingElectric.BuildField(Target_FieldName_Electric_Power)
        .SetFrom(Source_ClassName_Electric, Source_FieldName_Electric_Power, true);

    return mappingElectric;

}
...

Map the manual grinder content type

The process for the manual grinder is very similar, except it has no additional fields outside the schema.

Due to a bug, certain versions of the migration tool may require you to include a non-schema field in your content type, or have at least one field that uses the KX13 counterpart as a template.

If you experience errors related the data class of your XbyK content types during data migration, you may need to include something like the testField example in the following code. You can delete the field from XbyK after your successful data migration.

C#
GrinderClassMapping.cs

...
// Migrate manual grinders
private static MultiClassMapping BuildManualGrinderMapping()
{
    var mappingManual = new MultiClassMapping(Target_ClassName_Manual, target =>
    {
        target.ClassName = Target_ClassName_Manual;
        target.ClassTableName = Target_TableName_Manual;
        target.ClassDisplayName = Target_DisplayName_Manual;
        target.ClassType = ClassType.CONTENT_TYPE;
        target.ClassContentTypeType = ClassContentTypeType.WEBSITE;
    });

    mappingManual.BuildField(Target_FieldName_Manual_ID).AsPrimaryKey();

    mappingManual.UseResusableSchema(Target_ClassName_Schema);

    mappingManual.BuildField(Target_FieldName_Schema_PromotionTitle)
        .SetFrom(Source_ClassName_Manual, Source_FieldName_Manual_PromotionTitle);

    mappingManual.BuildField(Target_FieldName_Schema_PromotionDescription)
        .SetFrom(Source_ClassName_Manual, Source_FieldName_Manual_PromotionDescription);

    mappingManual.BuildField(Target_FieldName_Schema_BannerText)
        .SetFrom(Source_ClassName_Manual, Source_FieldName_Manual_BannerText);

    // Setting a non-schema field to the content type may be necessary in some versions of the migration tool.
    // You can delete this field from XbyK after data migration is complete
    var testField = mappingManual.BuildField("TestField");
    testField.ConvertFrom(Source_ClassName_Manual, Source_FieldName_Manual_PromotionTitle, true,
        (value, context) => "TestValue - Delete this field in XbyK");
    testField.WithFieldPatch(field => field.Caption = "Test - Delete this field in XbyK");

    return mappingManual;
}
...

Register the mappings

Now that the classes are mapped, you need to register the mappings and the schema builder with the dependency injection container.

Create a MigrateGrinders extension method for IServiceCollection that calls the private methods we’ve built so far and registers the resulting objects as singletons.

Then, call this method from the ServiceCollectionExtensions class of the Migration.Tools.Extensions project, which executes at startup.

C#
GrinderClassMapping.cs

...
public static IServiceCollection MigrateGrinders(this IServiceCollection serviceCollection)
{
    var schemaBuilder = BuildSchema();

    var mappingElectric = BuildElectricGrinderMapping();

    var mappingManual = BuildManualGrinderMapping();

    // Register with the DI container
    serviceCollection.AddSingleton<IClassMapping>(mappingElectric);
    serviceCollection.AddSingleton<IClassMapping>(mappingManual);

    serviceCollection.AddSingleton<IReusableSchemaBuilder>(schemaBuilder);

    return serviceCollection;
}
...
C#
ServiceCollectionExtensions.cs

...
using Migration.Tool.Extensions.ClassMappings;
...
public static class ServiceCollectionExtensions
{
    public static IServiceCollection UseCustomizations(this IServiceCollection services)
    {
        ...
        services.MigrateGrinders();
        ...

See the results

Now, run the data migration and you will see that both grinder content types use the reusable Grinder schema.

Electric grinder:

Screenshot of migrated electric grinder type

Manual grinder:

Screenshot of migrated manual grinder type

You can see that the new content types include a DocumentName field, corresponding to the field of the same name in KX13’s CMS.Document class. The migration tool automatically adds this field to content types created from pages, to hold the un-altered display name from the KX13 page.

If you have no use for this field, you can delete it from your content types in Xperience by Kentico.

What’s next?

Keep an eye out for future migration deep dives in this section of the guides.

If you encounter any roadblocks during your own migration, don’t hesitate to reach out to us through the Send us feedback button at the bottom of this page to let us know the scenarios you’d like us to explore in more detail.