Take the community feedback survey now.

KennyG
Aug 13, 2023
  1391
(4 votes)

Programmatically Exempt A PageType From Content Approval

We've got a use case that I think is a little advanced for what you get with Content Approval out of the box. We're a home builder and our commerce catalog is structured as Brand > State > City > Community > Lots (homes) and Plans.

While we want to require content approval at the community level (which numbers in the hundreds) we might not want to require approval at the lot and plan level (which numbers in the thousands). So if you set up Content Approval at the State level it is going to inherit all of the way down. We want to break it at certain points programmatically.

I've tested this out in a fresh Alloy site. Enable Content Approval at the About Us node and it will apply to the entire branch.

Let's exempt the NewsPage page type from Content Approval.

You can see that it gets its definition from the parent node.

We create an Initialization Module so we can hook into the the SavingContent event. 

        public void Initialize(InitializationEngine context)
        {
            _contentEvents = context.Locate.Advanced.GetInstance<IContentEvents>();
            _approvalDefinitionRepository = context.Locate.Advanced.GetInstance<IApprovalDefinitionRepository>();
            _contentRepository = context.Locate.Advanced.GetInstance<IContentRepository>();

            _contentEvents.SavingContent += ContentEvents_SavingContent_DisableContentApproval;
        }

        private async void ContentEvents_SavingContent_DisableContentApproval(object sender, ContentEventArgs e)
        {
            if (e.Content is NewsPage)
            {
                // Gets the latest version of a definition by resolving a ContentReference.  
                var definitionResolveResult = await _approvalDefinitionRepository.ResolveAsync(e.ContentLink);

                // The Resolve-method returns a result with a definition and a flag specifying if the definition was found on an ancestor
                var definition = definitionResolveResult.Definition as ContentApprovalDefinition;
                var isInherited = definitionResolveResult.IsInherited;

                if (definition != null && definition.IsEnabled)
                {
                    CreateOrUpdateDefinition(definition, e.ContentLink, false, isInherited);
                }
            }
        }

For any pagetype that is a NewsPage we use the ContentReference to get the definition (ApprovalDefinitionResolveResult) that applies to that specific node. ApprovalDefinitionResolveResult is an object that contains the Definition (ApprovalDefinition) and an IsInherited flag (bool) that signifies whether the definition comes from this node or is inherited from an ancestor.

    public class ApprovalDefinitionResolveResult
    {
        //
        // Summary:
        //     The resolved approval definition or null is no is found
        public ApprovalDefinition Definition { get; set; }

        //
        // Summary:
        //     Specifies if the definition was inherited from a content ancestor
        public bool IsInherited { get; set; }
    }

 We cast this definition as a ContentApprovalDefinition. We check to make sure the definition isn't null and that it is enabled (active). Then we pass it to a CreateOrUpdateDefinition method. When using this method if a definition does not exist it creates one, otherwise it updates it.

        private void CreateOrUpdateDefinition(ContentApprovalDefinition definition, ContentReference contentLink, bool isEnabled, bool isInherited)
        {
            if (definition == null)
            {
                CreateContentDefinition(contentLink, isEnabled);
                return;
            }

            //first pass definition down to any children to break inheritance
            var children = _contentRepository.GetChildren<PageData>(contentLink);

            if (children != null && children.Any())
            {

                foreach (var child in children)
                {
                    var definitionResolveResult = _approvalDefinitionRepository.ResolveAsync(child.ContentLink).Result;

                    if (definitionResolveResult.IsInherited)
                    {
                        UpdateDefinition(definitionResolveResult.Definition as ContentApprovalDefinition, child.ContentLink, definitionResolveResult.Definition.IsEnabled);
                    }
                }
            }

            //update page 
            UpdateDefinition(definition, contentLink, isEnabled);

        }

We check to see if the current node has any children, if so (and they are of a non-exempt page type) we need to pass the definition down to each one to break the inheritance. We loop through the children and update the definition for each one. Now that each child is now the root of its own approval branch we can update the node itself and turn off inheritance for that node by using the UpdateDefinition method.

        private void UpdateDefinition(ContentApprovalDefinition definition, ContentReference contentLink, bool isEnabled)
        {
            var newDefinition = definition.CreateWritableClone() as ContentApprovalDefinition;
            newDefinition.IsEnabled = isEnabled;
            newDefinition.ContentLink = contentLink;

            _approvalDefinitionRepository.SaveAsync(newDefinition).ConfigureAwait(false);
        }

We clone the definition that has been passed in (this definition comes from the ancestor), set it as enabled, give it the current node link, and save it. The method for creating a definition is very similar.

        private void CreateContentDefinition(ContentReference contentLink, bool isEnabled)
        {
            var newDefinition = new ContentApprovalDefinition
            {
                ContentLink = contentLink,
                IsEnabled = isEnabled
            };

            _approvalDefinitionRepository.SaveAsync(newDefinition).ConfigureAwait(false);
        }

Now you may be saying to yourself, none of this comes into effect until the page is saved. That's what is nice about this approach, you don't have to loop through the entire tree to apply it. It kicks in for any node of the exempt type when an editor starts editing and applies during the first AutoSave. 

Here is the entire Init Module:

using EPiServer.Approvals.ContentApprovals;
using EPiServer.Approvals;
using EPiServer.Framework;
using EPiServer.Framework.Initialization;
using EPiServer.ServiceLocation;
using EPiServer.Web;
using Opti_Alloy.Models.Pages;

namespace Opti_Alloy.Business.Initialization
{
    [ModuleDependency(typeof(InitializationModule))]
    public class ExemptPageTypesInitialization : IInitializableModule
    {
        private IContentEvents _contentEvents;
        private IApprovalDefinitionRepository _approvalDefinitionRepository;
        private IContentRepository _contentRepository;

        public void Initialize(InitializationEngine context)
        {
            _contentEvents = context.Locate.Advanced.GetInstance<IContentEvents>();
            _approvalDefinitionRepository = context.Locate.Advanced.GetInstance<IApprovalDefinitionRepository>();
            _contentRepository = context.Locate.Advanced.GetInstance<IContentRepository>();

            _contentEvents.SavingContent += ContentEvents_SavingContent_DisableContentApproval;
        }

        private async void ContentEvents_SavingContent_DisableContentApproval(object sender, ContentEventArgs e)
        {
            if (e.Content is NewsPage)
            {
                // Gets the latest version of a definition by resolving a ContentReference.  
                var definitionResolveResult = await _approvalDefinitionRepository.ResolveAsync(e.ContentLink);

                // The Resolve-method returns a result with a definition and a flag specifying if the definition was found on an ancestor
                var definition = definitionResolveResult.Definition as ContentApprovalDefinition;
                var isInherited = definitionResolveResult.IsInherited;

                if (definition != null && definition.IsEnabled)
                {
                    CreateOrUpdateDefinition(definition, e.ContentLink, false, isInherited);
                }
            }
        }

        private void CreateOrUpdateDefinition(ContentApprovalDefinition definition, ContentReference contentLink, bool isEnabled, bool isInherited)
        {
            if (definition == null)
            {
                CreateContentDefinition(contentLink, isEnabled);
                return;
            }

            //first pass definition down to any children to break inheritance
            var children = _contentRepository.GetChildren<PageData>(contentLink);

            if (children != null && children.Any())
            {

                foreach (var child in children)
                {
                    var definitionResolveResult = _approvalDefinitionRepository.ResolveAsync(child.ContentLink).Result;

                    if (definitionResolveResult.IsInherited)
                    {
                        UpdateDefinition(definitionResolveResult.Definition as ContentApprovalDefinition, child.ContentLink, definitionResolveResult.Definition.IsEnabled);
                    }
                }
            }

            //update page 
            UpdateDefinition(definition, contentLink, isEnabled);

        }

        private void UpdateDefinition(ContentApprovalDefinition definition, ContentReference contentLink, bool isEnabled)
        {
            var newDefinition = definition.CreateWritableClone() as ContentApprovalDefinition;
            newDefinition.IsEnabled = isEnabled;
            newDefinition.ContentLink = contentLink;

            _approvalDefinitionRepository.SaveAsync(newDefinition).ConfigureAwait(false);
        }

        private void CreateContentDefinition(ContentReference contentLink, bool isEnabled)
        {
            var newDefinition = new ContentApprovalDefinition
            {
                ContentLink = contentLink,
                IsEnabled = isEnabled
            };

            _approvalDefinitionRepository.SaveAsync(newDefinition).ConfigureAwait(false);
        }

        public void Uninitialize(InitializationEngine context)
        {
            _contentEvents.SavingContent -= ContentEvents_SavingContent_DisableContentApproval;
        }
    }
}

Going back to our Alloy example, if we edit something on the Events page (NewsPage) we can see that it is ready for direct publish, because the definitions were updated as soon as it Autosaved.

Checking the Content Approval settings, we see that the process is disabled at this node.

And then checking that node's children, we see that each of them is now the root of a new approval sequence copied from the ancestor.

There may be other approaches but I like that this one only kicks in when needed and can be done at any point in the branch. My initial attempt only worked for the end nodes (leaves) but this approach is much more flexible since it copies the definitions down to the children before updating the current node.

Let me know what you think in the comments!

Aug 13, 2023

Comments

Please login to comment.
Latest blogs
A day in the life of an Optimizely OMVP - What's New in Optimizely CMS: A Comprehensive Recap of 2025 Updates

Hello and welcome to another instalment of a day in the life of an Optimizely OMVP. On the back of the presentation I gave in the October 2025 happ...

Graham Carr | Nov 3, 2025

Optimizely CMS Mixed Auth - Okta + ASP.NET Identity

Configuring mixed authentication and authorization in Optimizely CMS using Okta and ASP.NET Identity.

Damian Smutek | Oct 27, 2025 |

Optimizely: Multi-Step Form Creation Through Submission

I have been exploring Optimizely Forms recently and created a multi-step Customer Support Request Form with File Upload Functionality.  Let’s get...

Madhu | Oct 25, 2025 |

How to Add Multiple Authentication Providers to an Optimizely CMS 12 Site (Entra ID, Google, Facebook, and Local Identity)

Modern websites often need to let users sign in with their corporate account (Entra ID), their social identity (Google, Facebook), or a simple...

Francisco Quintanilla | Oct 22, 2025 |

Connecting the Dots Between Research and Specification to Implementation using NotebookLM

Overview As part of my day to day role as a solution architect I overlap with many clients, partners, solutions and technologies. I am often...

Scott Reed | Oct 22, 2025

MimeKit Vulnerability and EPiServer.CMS.Core Dependency Update

Hi everyone, We want to inform you about a critical security vulnerability affecting older versions of the EPiServer.CMS.Core  package due to its...

Bien Nguyen | Oct 21, 2025