All code samples are based on the Alloy website and Episerver 11.

IValidate

Let's say we've created the following page type:

[SiteContentType(
    GroupName = Global.GroupNames.SiteSettings,
    GUID = "5FB4E44F-1AF5-4BDF-BC0E-C442091FB6D7",
    Description = "My page")]
[SiteImageUrl(
    Global.StaticGraphicsFolderPath + 
    "page-type-thumbnail-article.png")]
public class MyPage : ArticlePage
{
    // ...
}

The simplest way to prevent multiple page types instances is to create a validator like this::

public class SingleInstanceValidator : IValidate<MyPage>
{
    private readonly IContentLoader _contentLoader;

    public SingleInstanceValidator(IContentLoader contentLoader)
    {
        _contentLoader = contentLoader;
    }

    public IEnumerable<ValidationError> Validate(MyPage instance)
    {
        var existingInstance = _contentLoader
            .GetChildren<MyPage>(ContentReference.StartPage)
            .FirstOrDefault(x => !x.ContentLink.CompareToIgnoreWorkID(instance.ContentLink));

        if (existingInstance != null)
        {
            return new ValidationError[]
            {
                new ValidationError
                {
                    ErrorMessage = "You've already created an instance of 'MyPage'",
                    Severity = ValidationErrorSeverity.Error,
                    ValidationType = ValidationErrorType.StorageValidation
                }
            };
        }

        // all good
        return Enumerable.Empty<ValidationError>();
    }
}

The code is pretty straightforward.

Every time you try to publish an instance of MyPage, the code inside Validate method will be executed. If there's already a published instance of MyPage, you'll get an error message like this:

This approach works fine most of the time. However, once we create an instance of MyPage, it'd be nice to hide it from the Available Page Types list:

AvailableInEditMode (code first)

One way to hide your content types from site editors is to set AvailableInEditMode = false

[SiteContentType(
    GroupName = Global.GroupNames.SiteSettings,
    GUID = "5FB4E44F-1AF5-4BDF-BC0E-C442091FB6D7",
    AvailableInEditMode = false,
    Description = "My page")]
[SiteImageUrl(
    Global.StaticGraphicsFolderPath + 
    "page-type-thumbnail-article.png")]
public class MyPage : ArticlePage
{
    // ...
}

And voila! MyPage is no longer available in edit mode.

Using only code first approach has some disadvantages.

For example, first, you'll have to push the code with AvailableInEditMode = true. Once the code has been published and you've created an instance of MyPage in integration, preproduction, and production environment, you'll have to make a new deployment with AvailableInEditMode = false. And this will definitely make your senior code reviewers upset :)

AvailableInEditMode (admin mode)

All page types settings that we made using the code-first approach can be overridden from the admin mode.

Go to admin / Content Types / Page Types and find your page type.

IMO, a combination of code-first and admin approaches works best.

Decorate your page types with the ContentTypeAttribute and set AvailableInEditMode = false. Once the code is deployed to the target environment, change that setting in admin mode, create a page instance in edit mode, and revert settings in admin mode.

Initalization module

You can also create an initialization module that runs on application startup:

[InitializableModule]
[ModuleDependency(typeof(EPiServer.Web.InitializationModule))]
public class SingleInstanceModule : IInitializableModule
{
    public void Initialize(InitializationEngine context)
    {
        var contentRepository = context
            .Locate
            .Advanced
            .GetInstance<IContentRepository>();

        var existingInstance = contentRepository
            .GetChildren<MyPage>(ContentReference.StartPage)
            .FirstOrDefault();

        // all good
        if (existingInstance != null)
        {
            return;
        }

        // create a new page instance
        var myPage = contentRepository.GetDefault<MyPage>(ContentReference.StartPage);
        myPage.Name = "Test";
        contentRepository.Save(myPage, SaveAction.Publish, AccessLevel.NoAccess);

        // hide MyPage content type in edit mode
        var contentTypeRepostory = context
            .Locate
            .Advanced
            .GetInstance<IContentTypeRepository>();

        var myPageContentType = (ContentType) contentTypeRepostory
            .Load<MyPage>()
            .CreateWritableClone();
        myPageContentType.IsAvailable = false;
        contentTypeRepostory.Save(myPageContentType);
    }

    public void Uninitialize(InitializationEngine context)
    {
    }
}

And this how it looks in edit mode:

Initialization modules look fancy and seem like a perfect solution. However, keep in mind that they do increase application startup times, and always ask yourself if they're really necessary.

Migration Steps (Optimizely Commerce)

And if you're working with Optimizely Commerce, you can create a migration step that will be executed only once per database.

[ServiceConfiguration(typeof(IMigrationStep))]
public class MyPageMigrationSetup : IMigrationStep
{
    public int Order => 100;
    public string Name => "Create MyPage";
    public string Description => "Creates a single instance of MyPage";

    public bool Execute(IProgressMessenger progressMessenger)
    {
        var contentRepository = ServiceLocator.Current.GetInstance<IContentRepository>();
        var existingInstance = contentRepository
            .GetChildren<MyPage>(ContentReference.StartPage)
            .FirstOrDefault();

        // someone was faster than us
        if (existingInstance != null)
        {
            return true;
        }

        var myPage = contentRepository.GetDefault<MyPage>(ContentReference.StartPage);
        myPage.Name = "My page";
        contentRepository.Save(myPage, SaveAction.Publish, AccessLevel.NoAccess);

        // hide MyPage content type in edit mode
        var contentTypeRepostory = ServiceLocator.Current.GetInstance<IContentTypeRepository>();

        var myPageContentType = (ContentType)contentTypeRepostory
            .Load<MyPage>()
            .CreateWritableClone();

        myPageContentType.IsAvailable = false;

        contentTypeRepostory.Save(myPageContentType);
        return true;
    }
}

And this is how it looks in action:

What's your favorite approach? Leave your comments below :)