Disclaimer: This website requires Please enable JavaScript in your browser settings for the best experience.

HomeDev GuideRecipesAPI Reference
Dev GuideAPI ReferenceUser GuideGitHubNuGetDev CommunityOptimizely AcademySubmit a ticketLog In
Dev Guide

Content approvals

Describes the concepts for reviewing and approving content.

Approvals are a core system for the process of reviewing and approving. Specific approval types are built on top of this. The most common case is content approvals used in the editorial UI to review and approve content items before publishing, which is also the main reason the approval system exists. You can create custom approval types similar to how content approvals are defined, but this is currently not supported and is not covered here.

📘

Note

If you want to create content approvals with the CMS user interface, see Approve Content in the Manage Content section.

Create approval

An approval is created when something needs to get reviewed (for example, a content item before publishing) and is connected to a definition. You can create multiple approvals for each definition.

Running an approval is the process of stepping through the definition where each step's reviewers decide whether to approve or reject. Steps must be approved for the whole approval. If a step is rejected, then the whole approval is rejected.

A Uri-based reference identifies an approval, and it must have a definition.

A started approval is locked to the definition version that was the current version when the approval was started. It does not switch to a new definition version if the definition is changed, but new approvals will use the new version.

IApprovalRepository is the base interface for saving, deleting, and listing approvals and their decisions. IApprovalEngine is built on top of IApprovalRepository to streamline the handling of approvals. Use the repository for listing approvals and use the engine for handling approvals. The engine also raises events that can be hooked up in IApprovalEngineEvents.

Reviewers

You can implement the four-eyes principle. You can prevent users from approving their changes when configuring an approval sequence. The default value is false, letting users approve their changes.

When a user approves or rejects a content item, a validation is made to see if the user is part of the step, either as a user or as a member of a role. A call to the SecurityEntityProvider is used to validate if a user is part of a role. This will call the underlying user or role provider configured for the site (AspNet Identity Provider).

📘

Notes

It is only the role name that is part of the definition, not the users in the role. The validation to see if a user is part of a role is made at the moment it is needed. This means that a user can be added to a role or removed from one and that will affect an already started approval.

The underlying user/role providers can have their own restrictions. For example, that the user must log in for the roles to update.

Approval flow

The normal state flow for a content version that is being approved is as follows:

  • CheckedOut – The editor has created a version that is being edited.
  • AwaitingApproval – The editor is done and marks the version ready for approval.
  • Rejected – If some reviewer does not approve the version, it transitions to Rejected. After the editor has made changes, it can be set for approval again.
  • CheckedIn – The version is approved and is ready for publishing.
  • Published – The version is published.

Hook up events

[InitializableModule]
public class ApprovalLogger: IInitializableModule {
  private IApprovalEngineEvents _approvalEngineEvents;

  public void Initialize(InitializationEngine context) {
    _approvalEngineEvents = context.Locate.Advanced.GetInstance<IApprovalEngineEvents>();
    _approvalEngineEvents.Approved += OnApproved;
  }

  public void Uninitialize(InitializationEngine context) => _approvalEngineEvents.Approved -= OnApproved;

  private void OnApproved(ApprovalEventArgs e) => LogManager.GetLogger(typeof (ApprovalLogger)).Debug("Approve");
}

Start an approval

Content approval is not started by saving an approval but by saving a content item with SaveAction.RequestApproval. This automatically creates and saves a ContentApproval for this content item, if a definition can be resolved.

IContent content;
IContentRepository contentRepository;

var approvalContentLink = contentRepository.Save(content, SaveAction.RequestApproval);

IContent content;
IContentRepository contentRepository;
    
var approvalContentLink = contentRepository.Save(content, SaveAction.RequestApproval);

Abort an approval

This deletes an approval and raises an aborted event:

Approval approval;
IApprovalEngine approvalEngine;

 await approvalEngine.AbortAsync(approval.ID, "user");

Make a decision

Use the engine to decide whether to approve or reject a step or the whole approval. You can add an optional comment to explain the reason for the decision. If the decision finishes the approval (approves the last step or rejects a step), this comment is saved on the approval as CompletedComment.

Approval approval;
IApprovalEngine approvalEngine;

// Approve a step if user is part of the current definition step
await approvalEngine.ApproveAsync(approval.ID, "user", 1, ApprovalDecisionScope.Step);

// Reject a step if user is part of the current definition step, adding a comment
await approvalEngine.RejectAsync(approval.ID, "user", 1, ApprovalDecisionScope.Step, "This is why I did it");

// Force approve a step whether the user is part of the current definition step or not
await approvalEngine.ApproveAsync(approval.ID, "user", 1, ApprovalDecisionScope.ForceStep);

// Force approve the whole approval 
await approvalEngine.ApproveAsync(approval.ID, "user", 1, ApprovalDecisionScope.Force);

There are also several more explicitly named extension methods to the engine that can be used, for example ApproveStepAsync and ForceApproveAsync.

List approvals

Use the GetAsync or GetItemsAsync methods on IApprovalRepository to get specific approvals using ID or ContentReference:

using EPiServer.Approvals.ContentApprovals;

ContentReference contentLink;
IApprovalRepository approvalRepository;

var approval = await approvalRepository.GetAsync(contentLink);

The ListAsync method takes an ApprovalQuery or ContentApprovalQuery object which searches the approvals based on the specified query data and returns a filtered list. This list can be paged.

This example gets approvals in review for a user based on a specific definition.

using EPiServer.Approvals.ContentApprovals;

ContentApprovalDefinition definition;
IApprovalRepository approvalRepository;

var approvals = await approvalRepository.ListAsync(new ContentApprovalQuery {
    Username = "user",
    Status = ApprovalStatus.InReview,
    DefinitionID = definition.ID
});

This example lists decisions made for approval:

Approval approval;
IApprovalRepository approvalRepository;

var decisions = await approvalRepository.ListDecisionsAsync(approval.ID);

Create an approval definition

An approval definition is a series of steps to be approved in sequence. Each step has one or more reviewers. One reviewer in each step must approve the content before it is moved to the next step in the definition. If the site is multi-language, you can set the definition so that reviewers can approve different languages in a step.

Definitions are mostly static, and they are created and changed rarely.

A definition has a Uri-based reference to an object, usually a content item. A saved definition with a reference can be used for that reference and other references, such as a content item and its descendants. The resolve method searches for a definition and checks if a reference has access to a definition. In the case of content approvals, that means traversing the content tree upwards and looking for a definition.

Each time a definition is saved, a definition version is created. The latest saved version is the current definition and also the version returned when asking for a definition through IApprovalDefinitionRepository. It is also possible to get a specific version through the IApprovalDefinitionVersionRepository.

This example shows the creation and saving of a definition:

using EPiServer.Approvals.ContentApprovals;

ContentReference contentLink;
IApprovalDefinitionRepository definitionRepository;

var langEN = new CultureInfo[] {
  CultureInfo.GetCultureInfo("en")
};
var langSV = new CultureInfo[] {
  CultureInfo.GetCultureInfo("sv")
};

// Creates a content approval definition
var definition = new ContentApprovalDefinition {
  ContentLink = contentLink,
    Steps = new List<ApprovalDefinitionStep> {
      new ApprovalDefinitionStep("step1", new ApprovalDefinitionReviewer[] {
        new ApprovalDefinitionReviewer("user1a", langEN),
          new ApprovalDefinitionReviewer("user1b", langSV),
      }),
      new ApprovalDefinitionStep("step2", new ApprovalDefinitionReviewer[] {
        new ApprovalDefinitionReviewer("user2a", langEN.Union(langSV))
      })
    }
};

// Saves the definition
await definitionRepository.SaveAsync(definition);

Update an approval definition

In this example, a definition is updated with a reviewer in the second step. This reviewer can approve items in languages using CultureInfo.InvariantCulture.

using EPiServer.Approvals.ContentApprovals;

ContentReference contentLink;
IApprovalDefinitionRepository definitionRepository;

var langInvariant = new CultureInfo[] {
  CultureInfo.InvariantCulture
};

// Gets a definition
ApprovalDefinition definition = await definitionRepository.GetAsync(contentLink);
definition = definition.CreateWritableClone();
definition.Steps[1].Reviewers.Add(new ApprovalDefinitionReviewer("user2b", langInvariant));

// Saves a definition
await definitionRepository.SaveAsync(definition);

Add a role to an approval definition

In this example, a definition is updated with a role reviewer in the first step. This role can approve items in the Swedish language.

using EPiServer.Approvals.ContentApprovals;

ContentReference contentLink;
IApprovalDefinitionRepository definitionRepository;

var langSV = new CultureInfo[] {
  CultureInfo.GetCultureInfo("sv")
};

// Gets a definition
ApprovalDefinition definition = await definitionRepository.GetAsync(contentLink);
definition = definition.CreateWritableClone();
definition.Steps[0].Reviewers.Add(new ApprovalDefinitionReviewer("managers", langSV, ApprovalDefinitionReviewerType.Role));

// Saves a definition
await definitionRepository.SaveAsync(definition);

Delete an approval definition

This example deletes a definition using ID:

ApprovalDefinition definition;
IApprovalDefinitionRepository definitionRepository;
  // Deletes a definition
  await definitionRepository.DeleteAsync(definition.ID);

📘

Note

You cannot delete a definition if running approvals exist. All approval instances must be completed or aborted before deleting an approval definition.

Get an approval definition

Gets a definition in a couple of different ways:

using EPiServer.Approvals.ContentApprovals;

ContentReference contentLink;
ApprovalDefinition definition;
Approval approval;
IApprovalDefinitionRepository definitionRepository;
IApprovalDefinitionVersionRepository definitionVersionRepository;

// Gets the latest version of a definition using a definition id.
var definition1 = await definitionRepository.GetAsync(definition.ID);

// Gets a specific version of a definition using a version id.
var definition2 = await definitionVersionRepository.GetAsync(approval.VersionID);

// Gets the latest version of a definition using a ContentReference. 
var definition3 = await definitionRepository.GetAsync(contentLink);

// Gets the latest version of a definition by resolving a ContentReference.  
var definitionResolveResult = await definitionRepository.ResolveAsync(contentLink);
// The Resolve-method returns a result with a definition and a flag specifying if the definition was found on an ancestor
var definition4 = definitionResolveResult.Definition as ContentApprovalDefinition;
var isInherited = definitionResolveResult.IsInherited;

Content approvals namespace

The content approvals feature is accessed through the EPiServer.Approvals.ContentApprovals namespace, which gives access to content versions of the classes ApprovalDefinition and Approval, and also extension methods with ContentReferences in the repository interfaces. The ContentReference is converted to the Uri reference under the hood.

📘

Note

The methods in this namespace should only be used for content approvals. If another approval system for content is used that also wants to identify by ContentReference, it should have its own set of extension methods in its own namespace.

A definition can be created on a content item in a content tree and be used by the descendants of that content item. Use ResolveAsync to find a definition.

📘

Note

A content definition is shared for all versions/languages of a content item. Content approvals are created for a specific content version/language. That means it is possible to have multiple approvals for a content item running at the same time if they have different languages.

A content approval is started by setting the status of a content item to AwaitingApproval. The referenced content item is transitioned to status CheckedIn when content approval is approved. If rejected, the content item is transitioned to status Rejected.

Email notifications

You can configure emails for content approval notifications to be sent immediately or periodically as a batch.

📘

Note

You need to configure the SMTP settings for Optimizely Content Management System (CMS) to send out email notifications, see Configure your email server.

The behavior of content approval notification emails is sending users emails periodically. This means that notifications are batched into a single email sent regularly.

You can configure content approval notification emails to be sent immediately or periodically to users.

Because the content approval notification emails are sent periodically by default, configuration is only needed when this is not the desired behavior. To configure emails to be sent immediately, the ApprovalNotificationOptions must be configured accordingly. This should be done during application initialization. Configuring the dispatch interval for emails is handled in the scheduled job; see the notification dispatcher job section below.

The following is an example of how the options can be configured during application initialization. A module dependency on FrameworkInitialization is required to ensure correct execution order.

[InitializableModule]
[ModuleDependency(typeof (FrameworkInitialization))]
public class ApprovalNotificationInitialization: IConfigurableModule {
  public void ConfigureContainer(ServiceConfigurationContext context) {
    context.Services.Configure<ApprovalNotificationOptions>(options => options.Immediate = true);
  }
}

A scheduled job, Notification Dispatcher, dispatches notification emails at regular intervals. If content approval notification emails are configured to be sent periodically, then the configuration of the scheduled job determines the interval for email dispatches.

The job is enabled by default and set to run every 30 minutes.